diff --git a/cmd/ateapi/internal/actoridentity/actoridentity.go b/cmd/ateapi/internal/actoridentity/actoridentity.go index 963c2e807..7d3913b06 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity.go @@ -32,6 +32,7 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/actoridjwt" "github.com/agent-substrate/substrate/cmd/ateapi/internal/k8sjwt" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" "github.com/agent-substrate/substrate/internal/localca" "github.com/agent-substrate/substrate/internal/localjwtauthority" "github.com/agent-substrate/substrate/internal/resources" @@ -60,12 +61,13 @@ type Server struct { // store is the actor database. MintCert consults it to confirm the caller // is entitled to the actor it is asking for a credential for. - store store.Interface + store store.Interface + workers *workercache.Cache } var _ ateapipb.ActorIdentityServer = (*Server)(nil) -func New(clientJWTIssuer, clientJWTAudience, actorIDJWTPoolFile, actorIDCAPoolFile, workerCACerts string, httpClient *http.Client, store store.Interface) *Server { +func New(clientJWTIssuer, clientJWTAudience, actorIDJWTPoolFile, actorIDCAPoolFile, workerCACerts string, httpClient *http.Client, store store.Interface, workers *workercache.Cache) *Server { return &Server{ clientJWTIssuer: clientJWTIssuer, clientJWTAudience: clientJWTAudience, @@ -74,6 +76,7 @@ func New(clientJWTIssuer, clientJWTAudience, actorIDJWTPoolFile, actorIDCAPoolFi workerCACerts: workerCACerts, httpClient: httpClient, store: store, + workers: workers, } } @@ -85,9 +88,10 @@ func New(clientJWTIssuer, clientJWTAudience, actorIDJWTPoolFile, actorIDCAPoolFi // imported so that this package does not depend on controlapi for three // strings; if a third pkg that need these constants appears, they should move to a shared package. const ( - ateletTrustDomain = "cluster.local" - ateletNamespace = "ate-system" - ateletSA = "atelet" + ateletTrustDomain = "cluster.local" + ateletNamespace = "ate-system" + ateletSA = "atelet" + actorCertificateLifetime = time.Hour ) func (s *Server) MintJWT(ctx context.Context, req *ateapipb.MintJWTRequest) (*ateapipb.MintJWTResponse, error) { @@ -125,7 +129,6 @@ func (s *Server) MintJWT(ctx context.Context, req *ateapipb.MintJWTRequest) (*at if err != nil { return nil, fmt.Errorf("while unmarshaling signing pool: %w", err) } - // We only issue tokens with audience bindings. if len(req.GetAudience()) == 0 { return nil, fmt.Errorf("at least one audience must be requested") @@ -170,35 +173,30 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (* if err != nil { return nil, err } - - atespace := req.GetAtespace() - actorName := req.GetActorName() - - if atespace == "" || actorName == "" { - return nil, status.Errorf(codes.InvalidArgument, "atespace and actor_name are required") + if req.GetPurpose() != ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL { + return nil, status.Error(codes.InvalidArgument, "unsupported actor certificate purpose") } - actorRef := resources.ActorRef{Atespace: atespace, Name: actorName} - actor, err := s.authorizeActor(ctx, caller, actorRef) + if req.GetWorkerNamespace() == "" || req.GetWorkerPod() == "" || req.GetWorkerPodUid() == "" || req.GetExpectedActorUid() == "" { + return nil, status.Error(codes.InvalidArgument, "worker_namespace, worker_pod, worker_pod_uid, and expected_actor_uid are required") + } + actor, actorRef, err := s.authorizeActor(ctx, caller, req) if err != nil { return nil, err } + atespace, actorName := actorRef.Atespace, actorRef.Name - // The UID is taken from the actor database rather than from the request: - // req.actor_uid is caller-supplied and unverified, and the certificate must - // name the incarnation of the actor that is actually placed. A request - // that names a different incarnation is refused rather than silently - // upgraded, since the caller is asking for a credential it would not be - // able to use. + // Actor identity comes only from ateapi state. expected_actor_uid is a + // fail-closed guard against a request crossing an assignment change. actorUID := actor.GetMetadata().GetUid() if actorUID == "" { slog.ErrorContext(ctx, "MintCert: actor has no UID", slog.Any("actor", actorRef)) return nil, status.Errorf(codes.Internal, "actor has no UID") } - if reqUID := req.GetActorUid(); reqUID != "" && reqUID != actorUID { - slog.WarnContext(ctx, "MintCert denied: requested actor UID does not match the placed actor", - slog.Any("actor", actorRef), slog.String("requestedUID", reqUID)) - return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint certificates for this actor") + if req.GetExpectedActorUid() != actorUID { + slog.WarnContext(ctx, "MintCert refused: expected actor UID does not match the placed actor", + slog.Any("actor", actorRef), slog.String("expectedActorUID", req.GetExpectedActorUid())) + return nil, status.Error(codes.FailedPrecondition, "worker assignment changed while minting actor certificate") } // Load the CA pool for signing @@ -232,7 +230,7 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (* template := &x509.Certificate{ URIs: []*url.URL{spiffeURI}, NotBefore: time.Now().Add(-5 * time.Minute), - NotAfter: time.Now().Add(15 * time.Minute), + NotAfter: time.Now().Add(actorCertificateLifetime), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, BasicConstraintsValid: true, @@ -246,6 +244,7 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (* Atespace: atespace, ActorName: actorName, ActorUid: actorUID, + Purpose: substratex509.ActorIdentityPurposeAtunnel, }, template); err != nil { slog.ErrorContext(ctx, "Failed to add ActorIdentity extension", slog.Any("err", err)) return nil, status.Errorf(codes.Internal, "Failed to build certificate") @@ -269,7 +268,7 @@ func (s *Server) MintCert(ctx context.Context, req *ateapipb.MintCertRequest) (* }, nil } -// ateletCaller is the verified identity of an atelet that called MintCert. +// ateletCaller is the verified identity of an atelet requesting an actor credential. type ateletCaller struct { podName string nodeName string @@ -298,7 +297,7 @@ func authenticateAtelet(ctx context.Context) (*ateletCaller, error) { } leaf := tlsInfo.State.PeerCertificates[0] - // Only atelet may mint actor certificates. Everything else with a valid + // Only atelet may mint actor credentials. Everything else with a valid // pod-identity certificate — including the actor workloads themselves — is // rejected here. expected := (&url.URL{ @@ -307,57 +306,70 @@ func authenticateAtelet(ctx context.Context) (*ateletCaller, error) { Path: path.Join("ns", ateletNamespace, "sa", ateletSA), }).String() if len(leaf.URIs) == 0 || leaf.URIs[0].String() != expected { - slog.WarnContext(ctx, "MintCert denied: caller is not atelet", + slog.WarnContext(ctx, "ActorIdentity denied: caller is not atelet", slog.Any("uris", leaf.URIs), slog.String("expected", expected)) - return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint actor certificates") + return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint actor credentials") } identity, err := substratex509.PodIdentityFromCertificate(leaf) if err != nil { - slog.WarnContext(ctx, "MintCert denied: malformed PodIdentity extension", slog.Any("err", err)) - return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint actor certificates") + slog.WarnContext(ctx, "ActorIdentity denied: malformed PodIdentity extension", slog.Any("err", err)) + return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint actor credentials") } if identity == nil { - slog.WarnContext(ctx, "MintCert denied: certificate has no PodIdentity extension") - return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint actor certificates") + slog.WarnContext(ctx, "ActorIdentity denied: certificate has no PodIdentity extension") + return nil, status.Errorf(codes.PermissionDenied, "caller is not permitted to mint actor credentials") } return &ateletCaller{podName: identity.PodName, nodeName: identity.NodeName}, nil } -// authorizeActor reports whether caller may mint a credential for actorRef, -// returning the actor record the decision was made against. -// -// The rule is that the actor must be placed on a worker pod that lives on the -// caller's own node, and that worker must still agree it is hosting the actor. -// An atelet is therefore confined to the actors it is actually hosting, and an -// actor that has been suspended, paused or migrated elsewhere can no longer -// have credentials minted for it. -func (s *Server) authorizeActor(ctx context.Context, caller *ateletCaller, actorRef resources.ActorRef) (*ateapipb.Actor, error) { +// authorizeActor resolves the actor from the authenticated worker and verifies +// that the worker and actor still point at one another. Actor identity supplied +// by the requester never participates in this authorization decision. +func (s *Server) authorizeActor(ctx context.Context, caller *ateletCaller, req *ateapipb.MintCertRequest) (*ateapipb.Actor, resources.ActorRef, error) { // Denials are deliberately indistinguishable from each other: a caller that - // is not entitled to an actor should not be able to use this RPC to learn - // whether that actor exists, or where it is running. + // is not entitled to a worker should not learn its assignment. deny := func(reason string, args ...any) error { - slog.WarnContext(ctx, "MintCert denied: "+reason, - append([]any{slog.Any("actor", actorRef), slog.String("callerPod", caller.podName), slog.String("callerNode", caller.nodeName)}, args...)...) - return status.Errorf(codes.PermissionDenied, "caller is not permitted to mint certificates for this actor") + slog.WarnContext(ctx, "ActorIdentity denied: "+reason, + append([]any{slog.String("workerPod", req.GetWorkerNamespace()+"/"+req.GetWorkerPod()), slog.String("callerPod", caller.podName), slog.String("callerNode", caller.nodeName)}, args...)...) + return status.Errorf(codes.PermissionDenied, "caller is not permitted to mint credentials for this actor") } + worker, err := s.workers.Worker(req.GetWorkerNamespace(), req.GetWorkerPod()) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, resources.ActorRef{}, deny("worker not found") + } + slog.ErrorContext(ctx, "ActorIdentity: failed to read worker", slog.Any("err", err)) + return nil, resources.ActorRef{}, status.Error(codes.Internal, "failed to look up worker") + } + if worker.GetNodeName() != caller.nodeName { + return nil, resources.ActorRef{}, deny("worker is hosted on a different node", slog.String("workerNode", worker.GetNodeName())) + } + if worker.GetWorkerPodUid() != req.GetWorkerPodUid() { + return nil, resources.ActorRef{}, deny("worker Pod UID does not match", slog.String("workerPodUID", req.GetWorkerPodUid())) + } + + actorRef := resources.ActorRefFromObjectRef(worker.GetAssignment().GetActor()) + if actorRef == (resources.ActorRef{}) { + return nil, resources.ActorRef{}, deny("worker has no actor assignment") + } actor, err := s.store.GetActor(ctx, actorRef) if err != nil { if errors.Is(err, store.ErrNotFound) { - return nil, deny("actor not found") + return nil, resources.ActorRef{}, deny("assigned actor not found") } - slog.ErrorContext(ctx, "MintCert: failed to read actor", slog.Any("actor", actorRef), slog.Any("err", err)) - return nil, status.Errorf(codes.Internal, "failed to look up actor") + slog.ErrorContext(ctx, "ActorIdentity: failed to read actor", slog.Any("actor", actorRef), slog.Any("err", err)) + return nil, resources.ActorRef{}, status.Error(codes.Internal, "failed to look up actor") } // Deletion is only entered from SUSPENDED or CRASHED, both of which // have already released the worker, so the assignment check below would - // reject this too. It is kept because minting for better visbility and logging. + // reject this too. It is kept for better visibility and logging. if actor.GetStatus() == ateapipb.Actor_STATUS_DELETING { - slog.WarnContext(ctx, "MintCert refused: actor is being deleted", slog.Any("actor", actorRef)) - return nil, status.Errorf(codes.FailedPrecondition, "actor is being deleted") + slog.WarnContext(ctx, "ActorIdentity refused: actor is being deleted", slog.Any("actor", actorRef)) + return nil, resources.ActorRef{}, status.Error(codes.FailedPrecondition, "actor is being deleted") } // An actor placed on a worker always carries its placement fields. Missing @@ -365,29 +377,15 @@ func (s *Server) authorizeActor(ctx context.Context, caller *ateletCaller, actor // folded into deny(). assignment := actor.GetWorkerAssignment() if assignment == nil { - slog.ErrorContext(ctx, "MintCert: running actor has no worker assignment", slog.Any("actor", actorRef)) - return nil, status.Errorf(codes.FailedPrecondition, "actor has no worker assigned") + slog.ErrorContext(ctx, "ActorIdentity: running actor has no worker assignment", slog.Any("actor", actorRef)) + return nil, resources.ActorRef{}, status.Error(codes.FailedPrecondition, "actor has no worker assigned") } - podNamespace, podName := assignment.GetWorkerNamespace(), assignment.GetWorkerPod() - - worker, err := s.store.GetWorker(ctx, podNamespace, assignment.GetWorkerPool(), podName) - if err != nil { - if errors.Is(err, store.ErrNotFound) { - return nil, deny("worker hosting the actor not found", slog.String("workerPod", podNamespace+"/"+podName)) - } - slog.ErrorContext(ctx, "MintCert: failed to read worker", slog.Any("actor", actorRef), slog.Any("err", err)) - return nil, status.Errorf(codes.Internal, "failed to look up worker") - } - - if worker.GetNodeName() != caller.nodeName { - return nil, deny("actor is hosted on a different node", slog.String("actorNode", worker.GetNodeName())) - } - - // The worker must still agree that it is hosting this actor. - if assigned := worker.GetAssignment().GetActor(); resources.ActorRefFromObjectRef(assigned) != actorRef { - return nil, deny("worker is no longer assigned to the actor", - slog.String("workerAssignment", assigned.GetAtespace()+"/"+assigned.GetName())) + if assignment.GetWorkerNamespace() != worker.GetWorkerNamespace() || + assignment.GetWorkerPool() != worker.GetWorkerPool() || + assignment.GetWorkerPod() != worker.GetWorkerPod() || + assignment.GetWorkerPodUid() != worker.GetWorkerPodUid() { + return nil, resources.ActorRef{}, deny("actor no longer points to the requesting worker", slog.Any("actor", actorRef)) } - return actor, nil + return actor, actorRef, nil } diff --git a/cmd/ateapi/internal/actoridentity/actoridentity_test.go b/cmd/ateapi/internal/actoridentity/actoridentity_test.go index 68796a2e9..c61873a6e 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity_test.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity_test.go @@ -31,6 +31,7 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" "github.com/agent-substrate/substrate/internal/localca" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/substratex509" @@ -147,7 +148,16 @@ func newTestServer(t *testing.T, st store.Interface) *Server { t.Fatalf("write CA pool: %v", err) } - return New("issuer", "audience", "", poolFile, "", nil, st) + var workers *workercache.Cache + if st != nil { + workers = workercache.New(st, time.Hour) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + if err := workers.Start(ctx); err != nil { + t.Fatalf("start worker cache: %v", err) + } + } + return New("issuer", "audience", "", poolFile, "", nil, st, workers) } // newCSR returns a DER-encoded, correctly self-signed CSR. @@ -166,10 +176,25 @@ func newCSR(t *testing.T) []byte { return der } +func mintCertRequest(t *testing.T, actorUID string) *ateapipb.MintCertRequest { + t.Helper() + return &ateapipb.MintCertRequest{ + WorkerNamespace: testPodNS, + WorkerPod: testWorkerPod, + WorkerPodUid: "worker-uid", + ExpectedActorUid: actorUID, + CertificateSigningRequest: newCSR(t), + Purpose: ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL, + } +} + // actorFixture describes the actor/worker pair seeded into the store. type actorFixture struct { status ateapipb.Actor_Status workerNode string + // actorWorkerPod overrides the Pod named by the actor while leaving the + // requesting worker unchanged, simulating a stale reciprocal assignment. + actorWorkerPod string // assignedTo overrides the actor the worker claims to be hosting. The zero // value means the worker is assigned to the seeded actor. assignedTo resources.ActorRef @@ -194,10 +219,14 @@ func seedActor(t *testing.T, ctx context.Context, st store.Interface, f actorFix ActorTemplateName: "counter", } if !f.noPlacement { + workerPod := testWorkerPod + if f.actorWorkerPod != "" { + workerPod = f.actorWorkerPod + } actor.WorkerAssignment = &ateapipb.WorkerAssignment{ WorkerNamespace: testPodNS, WorkerPool: testPool, - WorkerPod: testWorkerPod, + WorkerPod: workerPod, WorkerPodUid: "worker-uid", } } @@ -250,9 +279,11 @@ func TestMintCertAuthorization(t *testing.T) { fixture actorFixture - // atespace and actorName override the request fields when non-nil. - atespace *string - actorName *string + // Request fields override their defaults when non-nil. + workerNamespace *string + workerPod *string + workerPodUID *string + expectedActorUID *string wantCode codes.Code }{ @@ -298,19 +329,30 @@ func TestMintCertAuthorization(t *testing.T) { wantCode: codes.PermissionDenied, }, "actor does not exist": { - fixture: runningOnNode(testNode), - actorName: ptr("no-such-actor"), - wantCode: codes.PermissionDenied, + fixture: actorFixture{ + status: ateapipb.Actor_STATUS_RUNNING, + workerNode: testNode, + assignedTo: resources.ActorRef{Atespace: testAtespace, Name: "no-such-actor"}, + }, + wantCode: codes.PermissionDenied, }, "actor exists under a different atespace": { - fixture: runningOnNode(testNode), - atespace: ptr("some-other-atespace"), + fixture: actorFixture{ + status: ateapipb.Actor_STATUS_RUNNING, + workerNode: testNode, + assignedTo: resources.ActorRef{Atespace: "some-other-atespace", Name: testActorName}, + }, wantCode: codes.PermissionDenied, }, "actor is hosted on a different node": { fixture: runningOnNode(testOtherNode), wantCode: codes.PermissionDenied, }, + "worker Pod UID does not match": { + fixture: runningOnNode(testNode), + workerPodUID: ptr("sibling-worker-uid"), + wantCode: codes.PermissionDenied, + }, "worker is assigned to a different actor": { fixture: actorFixture{ status: ateapipb.Actor_STATUS_RUNNING, @@ -319,6 +361,14 @@ func TestMintCertAuthorization(t *testing.T) { }, wantCode: codes.PermissionDenied, }, + "actor points to a different worker": { + fixture: actorFixture{ + status: ateapipb.Actor_STATUS_RUNNING, + workerNode: testNode, + actorWorkerPod: "replacement-worker", + }, + wantCode: codes.PermissionDenied, + }, "hosting worker record is missing": { fixture: actorFixture{ status: ateapipb.Actor_STATUS_RUNNING, @@ -330,8 +380,8 @@ func TestMintCertAuthorization(t *testing.T) { "actor has no placement": { fixture: actorFixture{ status: ateapipb.Actor_STATUS_RUNNING, + workerNode: testNode, noPlacement: true, - noWorker: true, }, wantCode: codes.FailedPrecondition, }, @@ -343,16 +393,21 @@ func TestMintCertAuthorization(t *testing.T) { }, wantCode: codes.PermissionDenied, }, - "atespace is empty": { - fixture: runningOnNode(testNode), - atespace: ptr(""), - wantCode: codes.InvalidArgument, + "worker namespace is empty": { + fixture: runningOnNode(testNode), + workerNamespace: ptr(""), + wantCode: codes.InvalidArgument, }, - "actor name is empty": { + "worker Pod is empty": { fixture: runningOnNode(testNode), - actorName: ptr(""), + workerPod: ptr(""), wantCode: codes.InvalidArgument, }, + "expected actor UID is empty": { + fixture: runningOnNode(testNode), + expectedActorUID: ptr(""), + wantCode: codes.InvalidArgument, + }, } { t.Run(name, func(t *testing.T) { ctx := context.Background() @@ -371,19 +426,24 @@ func TestMintCertAuthorization(t *testing.T) { callerCert = ateletCertOn(t, testNode) } - atespace, actorName := testAtespace, testActorName - if tc.atespace != nil { - atespace = *tc.atespace + actor, err := st.GetActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: testActorName}) + if err != nil { + t.Fatalf("read seeded actor: %v", err) } - if tc.actorName != nil { - actorName = *tc.actorName + req := mintCertRequest(t, actor.GetMetadata().GetUid()) + if tc.workerNamespace != nil { + req.WorkerNamespace = *tc.workerNamespace } - - resp, err := srv.MintCert(ctxWithCert(callerCert), &ateapipb.MintCertRequest{ - Atespace: atespace, - ActorName: actorName, - CertificateSigningRequest: newCSR(t), - }) + if tc.workerPod != nil { + req.WorkerPod = *tc.workerPod + } + if tc.workerPodUID != nil { + req.WorkerPodUid = *tc.workerPodUID + } + if tc.expectedActorUID != nil { + req.ExpectedActorUid = *tc.expectedActorUID + } + resp, err := srv.MintCert(ctxWithCert(callerCert), req) if got := status.Code(err); got != tc.wantCode { t.Fatalf("MintCert() code = %v (err = %v), want %v", got, err, tc.wantCode) } @@ -409,6 +469,21 @@ func TestMintCertAuthorization(t *testing.T) { } } +func TestMintCertRejectsUnsupportedPurpose(t *testing.T) { + server := newTestServer(t, nil) + for name, purpose := range map[string]ateapipb.ActorCertificatePurpose{ + "unspecified": ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED, + "unknown": ateapipb.ActorCertificatePurpose(99), + } { + t.Run(name, func(t *testing.T) { + _, err := server.MintCert(ctxWithCert(ateletCertOn(t, testNode)), &ateapipb.MintCertRequest{Purpose: purpose}) + if got := status.Code(err); got != codes.InvalidArgument { + t.Fatalf("MintCert() code = %v (err = %v), want %v", got, err, codes.InvalidArgument) + } + }) + } +} + // mintCertFor seeds a running actor and mints a certificate for it, returning // the parsed leaf alongside the UID the store assigned the actor. The request // is built from that UID, since it is only known once the actor exists. @@ -446,12 +521,8 @@ func mintCertFor(t *testing.T, request func(actorUID string) *ateapipb.MintCertR // TestMintCertEmbedsActorIdentity checks that a minted certificate carries the // ActorIdentity extension, naming the actor the store knows about. func TestMintCertEmbedsActorIdentity(t *testing.T) { - leaf, actorUID, err := mintCertFor(t, func(string) *ateapipb.MintCertRequest { - return &ateapipb.MintCertRequest{ - Atespace: testAtespace, - ActorName: testActorName, - CertificateSigningRequest: newCSR(t), - } + leaf, actorUID, err := mintCertFor(t, func(actorUID string) *ateapipb.MintCertRequest { + return mintCertRequest(t, actorUID) }) if err != nil { t.Fatalf("MintCert(): %v", err) @@ -468,35 +539,29 @@ func TestMintCertEmbedsActorIdentity(t *testing.T) { Atespace: testAtespace, ActorName: testActorName, ActorUid: actorUID, + Purpose: substratex509.ActorIdentityPurposeAtunnel, } if *got != *want { t.Errorf("ActorIdentity = %+v, want %+v", got, want) } } -// TestMintCertActorUID checks how the caller-supplied actor_uid is treated: it -// is honored when it agrees with the store, ignored when absent, and refused -// when it names some other incarnation of the actor. In no case does it decide -// what goes into the certificate — that always comes from the store. +// TestMintCertActorUID checks that expected_actor_uid rejects a request that +// crossed an actor reassignment. It never decides the certificate identity, +// which always comes from ateapi state. func TestMintCertActorUID(t *testing.T) { for name, tc := range map[string]struct { - // requestUID derives the actor_uid the caller sends from the UID the - // store assigned the seeded actor. requestUID func(actorUID string) string wantCode codes.Code }{ - "Omitted": {requestUID: func(string) string { return "" }, wantCode: codes.OK}, "Matching": {requestUID: func(actorUID string) string { return actorUID }, wantCode: codes.OK}, - "Stale": {requestUID: func(string) string { return "uid-of-a-previous-incarnation" }, wantCode: codes.PermissionDenied}, + "Stale": {requestUID: func(string) string { return "uid-of-a-previous-incarnation" }, wantCode: codes.FailedPrecondition}, } { t.Run(name, func(t *testing.T) { leaf, actorUID, err := mintCertFor(t, func(actorUID string) *ateapipb.MintCertRequest { - return &ateapipb.MintCertRequest{ - Atespace: testAtespace, - ActorName: testActorName, - ActorUid: tc.requestUID(actorUID), - CertificateSigningRequest: newCSR(t), - } + req := mintCertRequest(t, actorUID) + req.ExpectedActorUid = tc.requestUID(actorUID) + return req }) if got := status.Code(err); got != tc.wantCode { t.Fatalf("MintCert() code = %v (err = %v), want %v", got, err, tc.wantCode) @@ -548,11 +613,11 @@ func TestMintCertActorStatus(t *testing.T) { seedActor(t, ctx, st, actorFixture{status: actorStatus, workerNode: testNode}) srv := newTestServer(t, st) - _, err := srv.MintCert(ctxWithCert(ateletCertOn(t, testNode)), &ateapipb.MintCertRequest{ - Atespace: testAtespace, - ActorName: testActorName, - CertificateSigningRequest: newCSR(t), - }) + actor, err := st.GetActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: testActorName}) + if err != nil { + t.Fatal(err) + } + _, err = srv.MintCert(ctxWithCert(ateletCertOn(t, testNode)), mintCertRequest(t, actor.GetMetadata().GetUid())) if got := status.Code(err); got != wantCode { t.Errorf("MintCert() code = %v (err = %v), want %v", got, err, wantCode) } @@ -583,11 +648,11 @@ func TestMintCertDeniesUnassignedActorWhateverItsStatus(t *testing.T) { }) srv := newTestServer(t, st) - _, err := srv.MintCert(ctxWithCert(ateletCertOn(t, testNode)), &ateapipb.MintCertRequest{ - Atespace: testAtespace, - ActorName: testActorName, - CertificateSigningRequest: newCSR(t), - }) + actor, err := st.GetActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: testActorName}) + if err != nil { + t.Fatal(err) + } + _, err = srv.MintCert(ctxWithCert(ateletCertOn(t, testNode)), mintCertRequest(t, actor.GetMetadata().GetUid())) if got := status.Code(err); got != codes.PermissionDenied { t.Errorf("MintCert() code = %v (err = %v), want %v", got, err, codes.PermissionDenied) } @@ -608,13 +673,21 @@ func TestMintCertAuthorizesBeforeSigning(t *testing.T) { // A server whose CA pool file does not exist: reaching the signing path at // all would surface as Internal rather than PermissionDenied. - srv := New("issuer", "audience", "", filepath.Join(t.TempDir(), "missing.json"), "", nil, st) + workers := workercache.New(st, time.Hour) + cacheCtx, cancel := context.WithCancel(ctx) + defer cancel() + if err := workers.Start(cacheCtx); err != nil { + t.Fatal(err) + } + srv := New("issuer", "audience", "", filepath.Join(t.TempDir(), "missing.json"), "", nil, st, workers) - _, err := srv.MintCert(ctxWithCert(ateletCertOn(t, testNode)), &ateapipb.MintCertRequest{ - Atespace: testAtespace, - ActorName: testActorName, - CertificateSigningRequest: []byte("not a CSR"), - }) + actor, err := st.GetActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: testActorName}) + if err != nil { + t.Fatal(err) + } + req := mintCertRequest(t, actor.GetMetadata().GetUid()) + req.CertificateSigningRequest = []byte("not a CSR") + _, err = srv.MintCert(ctxWithCert(ateletCertOn(t, testNode)), req) if got := status.Code(err); got != codes.PermissionDenied { t.Errorf("MintCert() code = %v (err = %v), want %v", got, err, codes.PermissionDenied) } diff --git a/cmd/ateapi/internal/controlapi/functional_test.go b/cmd/ateapi/internal/controlapi/functional_test.go index 84bb5c92a..3ce1c5457 100644 --- a/cmd/ateapi/internal/controlapi/functional_test.go +++ b/cmd/ateapi/internal/controlapi/functional_test.go @@ -332,7 +332,7 @@ func setupTest(t *testing.T, ns string) *testContext { mr.Close() t.Fatalf("failed to create metric instruments: %v", err) } - service := NewService(persistence, wc, actorTemplateLister, workerPoolLister, sandboxConfigLister, dialer, k8sClient, instruments) + service := NewService(persistence, wc, actorTemplateLister, workerPoolLister, sandboxConfigLister, dialer, k8sClient, instruments, "") // 5. Start REAL gRPC Server for ATE API grpcServer := grpc.NewServer(grpc.UnaryInterceptor(ateinterceptors.ServerUnaryInterceptor)) diff --git a/cmd/ateapi/internal/controlapi/service.go b/cmd/ateapi/internal/controlapi/service.go index 30cfc55e7..ee4d3bb71 100644 --- a/cmd/ateapi/internal/controlapi/service.go +++ b/cmd/ateapi/internal/controlapi/service.go @@ -26,6 +26,7 @@ import ( type Service struct { ateapipb.UnimplementedControlServer persistence store.Interface + workerCache *workercache.Cache dialer *AteletDialer actorTemplateLister listersv1alpha1.ActorTemplateLister workerPoolLister listersv1alpha1.WorkerPoolLister @@ -45,13 +46,15 @@ func NewService( dialer *AteletDialer, kubeClient kubernetes.Interface, instruments *Instruments, + egressGatewayAddress string, ) *Service { s := &Service{ persistence: persistence, + workerCache: workerCache, actorTemplateLister: actorTemplateLister, workerPoolLister: workerPoolLister, dialer: dialer, - actorWorkflow: NewActorWorkflow(persistence, workerCache, dialer, actorTemplateLister, workerPoolLister, sandboxConfigLister, kubeClient, instruments), + actorWorkflow: NewActorWorkflow(persistence, workerCache, dialer, actorTemplateLister, workerPoolLister, sandboxConfigLister, kubeClient, instruments, egressGatewayAddress), instruments: instruments, } return s diff --git a/cmd/ateapi/internal/controlapi/workflow.go b/cmd/ateapi/internal/controlapi/workflow.go index a62e954f6..2f9fa4540 100644 --- a/cmd/ateapi/internal/controlapi/workflow.go +++ b/cmd/ateapi/internal/controlapi/workflow.go @@ -132,16 +132,17 @@ func runStep[Params any, Context any](ctx context.Context, params Params, wCtx C // ActorWorkflow handles the workflows for actor's resume / suspend operations. type ActorWorkflow struct { - store store.Interface - workerCache *workercache.Cache - scheduler scheduling.Scheduler - dialer *AteletDialer - actorTemplateLister listersv1alpha1.ActorTemplateLister - workerPoolLister listersv1alpha1.WorkerPoolLister - sandboxConfigLister listersv1alpha1.SandboxConfigLister - kubeClient kubernetes.Interface - secretCache *envSecretCache - instruments *Instruments + store store.Interface + workerCache *workercache.Cache + scheduler scheduling.Scheduler + dialer *AteletDialer + actorTemplateLister listersv1alpha1.ActorTemplateLister + workerPoolLister listersv1alpha1.WorkerPoolLister + sandboxConfigLister listersv1alpha1.SandboxConfigLister + kubeClient kubernetes.Interface + secretCache *envSecretCache + instruments *Instruments + egressGatewayAddress string } // NewActorWorkflow creates a new ActorWorkflow. instruments may be nil. @@ -154,18 +155,20 @@ func NewActorWorkflow( sandboxConfigLister listersv1alpha1.SandboxConfigLister, kubeClient kubernetes.Interface, instruments *Instruments, + egressGatewayAddress string, ) *ActorWorkflow { return &ActorWorkflow{ - store: store, - workerCache: workerCache, - scheduler: scheduling.New(workerCache), - dialer: dialer, - actorTemplateLister: actorTemplateLister, - workerPoolLister: workerPoolLister, - sandboxConfigLister: sandboxConfigLister, - kubeClient: kubeClient, - secretCache: newEnvSecretCache(envSecretCacheTTL), - instruments: instruments, + store: store, + workerCache: workerCache, + scheduler: scheduling.New(workerCache), + dialer: dialer, + actorTemplateLister: actorTemplateLister, + workerPoolLister: workerPoolLister, + sandboxConfigLister: sandboxConfigLister, + kubeClient: kubeClient, + secretCache: newEnvSecretCache(envSecretCacheTTL), + instruments: instruments, + egressGatewayAddress: egressGatewayAddress, } } @@ -201,7 +204,7 @@ func (w *ActorWorkflow) ResumeActor(ctx context.Context, actorRef resources.Acto &CreateVolumesStep{store: w.store}, &AssignWorkerStep{store: w.store, workerCache: w.workerCache, scheduler: w.scheduler, instruments: w.instruments}, &AttachVolumesStep{store: w.store}, - &CallAteletRestoreStep{store: w.store, dialer: w.dialer, kubeClient: w.kubeClient, secretCache: w.secretCache, workerPoolLister: w.workerPoolLister, sandboxConfigLister: w.sandboxConfigLister, scheduler: w.scheduler}, + &CallAteletRestoreStep{store: w.store, dialer: w.dialer, kubeClient: w.kubeClient, secretCache: w.secretCache, workerPoolLister: w.workerPoolLister, sandboxConfigLister: w.sandboxConfigLister, scheduler: w.scheduler, egressGatewayAddress: w.egressGatewayAddress}, &FinalizeRunningStep{store: w.store}, } diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index ef4004c62..691f833dd 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -483,13 +483,14 @@ func (s *AttachVolumesStep) Execute(ctx context.Context, input *ResumeInput, sta func (s *AttachVolumesStep) RetryBackoff() *wait.Backoff { return nil } type CallAteletRestoreStep struct { - store store.Interface - dialer *AteletDialer - kubeClient kubernetes.Interface - secretCache *envSecretCache - workerPoolLister listersv1alpha1.WorkerPoolLister - sandboxConfigLister listersv1alpha1.SandboxConfigLister - scheduler scheduling.Scheduler + store store.Interface + dialer *AteletDialer + kubeClient kubernetes.Interface + secretCache *envSecretCache + workerPoolLister listersv1alpha1.WorkerPoolLister + sandboxConfigLister listersv1alpha1.SandboxConfigLister + scheduler scheduling.Scheduler + egressGatewayAddress string } func (s *CallAteletRestoreStep) Name() string { return "CallAteletRestore" } @@ -548,6 +549,7 @@ func (s *CallAteletRestoreStep) Execute(ctx context.Context, input *ResumeInput, if err != nil { return err } + egressGateway := s.egressGateway() if local := state.Actor.GetLocalSnapshotInfo(); local != nil { slog.InfoContext(ctx, "Actor has snapshot; Restoring from snapshot") @@ -561,6 +563,7 @@ func (s *CallAteletRestoreStep) Execute(ctx context.Context, input *ResumeInput, ActorTemplateName: state.Actor.GetActorTemplateName(), Spec: workloadSpec, ActorUid: state.Actor.GetMetadata().Uid, + EgressGateway: egressGateway, } req.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL req.Config = &ateletpb.RestoreRequest_LocalConfig{ @@ -613,6 +616,7 @@ func (s *CallAteletRestoreStep) Execute(ctx context.Context, input *ResumeInput, // Empty unless this is a Golden data resume. GoldenSnapshotUriPrefix: state.GoldenSnapshotLocation, ActorUid: state.Actor.GetMetadata().Uid, + EgressGateway: egressGateway, } _, err = client.Restore(ctx, req) return maybeCrashActor(ctx, s.store, input.ActorRef, err, "while restoring durable snapshot", ateattr.OperationResume) @@ -638,6 +642,7 @@ func (s *CallAteletRestoreStep) Execute(ctx context.Context, input *ResumeInput, SandboxAssets: sandboxAssets, Spec: workloadSpec, ActorUid: state.Actor.GetMetadata().Uid, + EgressGateway: egressGateway, } _, err = client.Run(ctx, req) return maybeCrashActor(ctx, s.store, input.ActorRef, err, "while creating workload from spec", ateattr.OperationResume) @@ -647,6 +652,13 @@ func (s *CallAteletRestoreStep) Execute(ctx context.Context, input *ResumeInput, func (s *CallAteletRestoreStep) RetryBackoff() *wait.Backoff { return nil } +func (s *CallAteletRestoreStep) egressGateway() *ateletpb.EgressGateway { + if s.egressGatewayAddress == "" { + return nil + } + return &ateletpb.EgressGateway{Address: s.egressGatewayAddress} +} + type FinalizeRunningStep struct { store store.Interface } diff --git a/cmd/ateapi/internal/controlapi/workflow_testutil_test.go b/cmd/ateapi/internal/controlapi/workflow_testutil_test.go index 997c20fcf..e941fcd81 100644 --- a/cmd/ateapi/internal/controlapi/workflow_testutil_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_testutil_test.go @@ -42,7 +42,7 @@ func newTestActorWorkflow(t *testing.T, st store.Interface, tmplNamespace, tmplN }); err != nil { t.Fatalf("add template to indexer: %v", err) } - return NewActorWorkflow(st, nil, nil, listersv1alpha1.NewActorTemplateLister(indexer), nil, nil, nil, nil) + return NewActorWorkflow(st, nil, nil, listersv1alpha1.NewActorTemplateLister(indexer), nil, nil, nil, nil, "") } // seedWorkflowActor stores an actor with the given status, bound to the given diff --git a/cmd/ateapi/internal/workercache/workercache.go b/cmd/ateapi/internal/workercache/workercache.go index 81b176573..b153217e4 100644 --- a/cmd/ateapi/internal/workercache/workercache.go +++ b/cmd/ateapi/internal/workercache/workercache.go @@ -89,6 +89,20 @@ func (c *Cache) Workers() ([]*ateapipb.Worker, error) { return out, nil } +// Worker returns the worker for a Kubernetes namespace and Pod name. +func (c *Cache) Worker(namespace, pod string) (*ateapipb.Worker, error) { + if !c.ready.Load() { + return nil, fmt.Errorf("worker cache not ready") + } + c.mu.RLock() + defer c.mu.RUnlock() + worker, ok := c.workers[namespace+":"+pod] + if !ok { + return nil, store.ErrNotFound + } + return worker, nil +} + func (c *Cache) sync(ctx context.Context) (*store.WorkerWatch, error) { watch, err := c.store.WatchWorkers(ctx) if err != nil { diff --git a/cmd/ateapi/internal/workercache/workercache_test.go b/cmd/ateapi/internal/workercache/workercache_test.go index cda1b8aea..e7a861439 100644 --- a/cmd/ateapi/internal/workercache/workercache_test.go +++ b/cmd/ateapi/internal/workercache/workercache_test.go @@ -36,6 +36,9 @@ func TestCache_NotReadyBeforeStart(t *testing.T) { if err == nil { t.Fatal("expected error from Workers before Start, got nil") } + if _, err := c.Worker("ns", "pod"); err == nil { + t.Fatal("expected error from Worker before Start, got nil") + } } func TestCache_SyncsOnStart(t *testing.T) { @@ -58,6 +61,24 @@ func TestCache_SyncsOnStart(t *testing.T) { } } +func TestCache_Worker(t *testing.T) { + want := makeWorker("ns", "pod", 1) + c := workercache.New(newFakeStore(want), time.Hour) + if err := c.Start(t.Context()); err != nil { + t.Fatalf("Start: %v", err) + } + got, err := c.Worker("ns", "pod") + if err != nil { + t.Fatalf("Worker: %v", err) + } + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("worker mismatch (-want +got):\n%s", diff) + } + if _, err := c.Worker("ns", "missing"); !errors.Is(err, store.ErrNotFound) { + t.Fatalf("missing Worker error = %v, want store.ErrNotFound", err) + } +} + func TestCache_CreatedEvent(t *testing.T) { fs := newFakeStore() c := workercache.New(fs, time.Hour) diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index cfb890ac3..e6c9980e2 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -70,9 +70,10 @@ var ( redisTLSServerName = pflag.String("redis-tls-server-name", "", "The ServerName to use for Redis TLS hostname verification.") redisClientCert = pflag.String("redis-client-cert", "", "The file containing client TLS certificate/key credential bundle for Redis/Valkey.") - 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") + 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.") actorIDCAPoolFile = pflag.String("actor-id-ca-pool", "", "The file that contains the CA pool for signing actor JWTs") podIdentityCACerts = pflag.String("pod-identity-ca-certs", "", "The file that contains the pod-identity CA bundle, used both for verifying client certificates presented to the gRPC server and for verifying atelet serving certificates when dialing atelet. If empty, client-cert verification is disabled and atelet dials will fail.") @@ -178,11 +179,11 @@ func main() { } ateletDialer := controlapi.NewAteletDialer(workerPodInformer.GetIndexer(), ateletPodInformer.GetIndexer(), *ateletClientCredBundle, *podIdentityCACerts) - sm := controlapi.NewService(redisPersistence, workerCache, actorTemplateLister, workerPoolLister, sandboxConfigLister, ateletDialer, clientset, instruments) + sm := controlapi.NewService(redisPersistence, workerCache, actorTemplateLister, workerPoolLister, sandboxConfigLister, ateletDialer, clientset, instruments, *egressGatewayAddress) jwtIssuerDiscoveryClient := buildK8sServiceAccountIssuerDiscoveryClient(ctx, *clientJWTCAFile, *clientJWTIssuer) - actorIdentitySrv := actoridentity.New(*clientJWTIssuer, *clientJWTAudience, *actorIDJWTPoolFile, *actorIDCAPoolFile, *podIdentityCACerts, jwtIssuerDiscoveryClient, redisPersistence) + actorIdentitySrv := actoridentity.New(*clientJWTIssuer, *clientJWTAudience, *actorIDJWTPoolFile, *actorIDCAPoolFile, *podIdentityCACerts, jwtIssuerDiscoveryClient, redisPersistence, workerCache) debugSrv := debugapi.NewService(redisPersistence) lisCfg := &net.ListenConfig{} diff --git a/cmd/atelet/credentialbroker.go b/cmd/atelet/credentialbroker.go new file mode 100644 index 000000000..fb49ff88b --- /dev/null +++ b/cmd/atelet/credentialbroker.go @@ -0,0 +1,93 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "crypto/tls" + "fmt" + + "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/substratex509" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +type credentialBroker struct { + ateletpb.UnimplementedCredentialBrokerServer + // actorIdentityClient resolves the authenticated worker's current assignment + // and signs its actor certificate. + actorIdentityClient ateapipb.ActorIdentityClient +} + +func (b *credentialBroker) MintActorCertificate(ctx context.Context, req *ateletpb.MintActorCertificateRequest) (*ateletpb.MintActorCertificateResponse, error) { + // TODO: Before release, require the egress PEP to reject actor certificates + // whose ActorIdentity purpose is not atunnel. + // Worker identity comes only from the mTLS certificate. The expected actor + // UID is a stale-activation guard; ateapi derives the actor authoritatively. + workerIdentity, err := authenticatedWorkerIdentity(ctx) + if err != nil { + return nil, err + } + resp, err := b.actorIdentityClient.MintCert(ctx, &ateapipb.MintCertRequest{ + WorkerNamespace: workerIdentity.Namespace, + WorkerPod: workerIdentity.PodName, + WorkerPodUid: workerIdentity.PodUID, + ExpectedActorUid: req.GetExpectedActorUid(), + CertificateSigningRequest: req.GetCertificateSigningRequest(), + Purpose: ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL, + }) + if err != nil { + return nil, fmt.Errorf("mint actor certificate: %w", err) + } + return &ateletpb.MintActorCertificateResponse{ActorCertificates: resp.GetActorCertificates()}, nil +} + +func authenticatedWorkerIdentity(ctx context.Context) (*substratex509.PodIdentity, error) { + p, ok := peer.FromContext(ctx) + if !ok { + return nil, status.Error(codes.Unauthenticated, "missing peer credentials") + } + tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo) + if !ok || len(tlsInfo.State.PeerCertificates) == 0 { + return nil, status.Error(codes.Unauthenticated, "missing peer certificate") + } + identity, err := substratex509.PodIdentityFromCertificate(tlsInfo.State.PeerCertificates[0]) + if err != nil || identity == nil { + return nil, status.Error(codes.PermissionDenied, "invalid worker identity") + } + return identity, nil +} + +// verifyClientOnSameNode returns a TLS callback that accepts only worker Pods +// scheduled on the atelet's node incarnation. +func verifyClientOnSameNode(node *substratex509.PodIdentity) func(tls.ConnectionState) error { + return func(state tls.ConnectionState) error { + if len(state.PeerCertificates) == 0 { + return fmt.Errorf("worker certificate is required") + } + identity, err := substratex509.PodIdentityFromCertificate(state.PeerCertificates[0]) + if err != nil { + return fmt.Errorf("parse worker Pod identity: %w", err) + } + if identity == nil || identity.NodeName != node.NodeName || identity.NodeUID != node.NodeUID { + return fmt.Errorf("worker is not on node %q (%s)", node.NodeName, node.NodeUID) + } + return nil + } +} diff --git a/cmd/atelet/credentialbroker_test.go b/cmd/atelet/credentialbroker_test.go new file mode 100644 index 000000000..8dc6998ea --- /dev/null +++ b/cmd/atelet/credentialbroker_test.go @@ -0,0 +1,108 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "math/big" + "testing" + "time" + + "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/substratex509" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + "google.golang.org/protobuf/proto" +) + +type brokerIdentityClient struct { + ateapipb.ActorIdentityClient + request *ateapipb.MintCertRequest +} + +func (c *brokerIdentityClient) MintCert(_ context.Context, req *ateapipb.MintCertRequest, _ ...grpc.CallOption) (*ateapipb.MintCertResponse, error) { + c.request = req + return &ateapipb.MintCertResponse{ActorCertificates: [][]byte{{1, 2, 3}}}, nil +} + +func TestCredentialBrokerForwardsAuthenticatedWorkerIdentity(t *testing.T) { + identity := &brokerIdentityClient{} + broker := &credentialBroker{actorIdentityClient: identity} + csr := []byte{4, 5, 6} + resp, err := broker.MintActorCertificate(workerContext(t, "worker-uid"), &ateletpb.MintActorCertificateRequest{ + CertificateSigningRequest: csr, + ExpectedActorUid: "actor-uid", + }) + if err != nil { + t.Fatal(err) + } + if !proto.Equal(resp, &ateletpb.MintActorCertificateResponse{ActorCertificates: [][]byte{{1, 2, 3}}}) { + t.Fatalf("response = %+v", resp) + } + want := &ateapipb.MintCertRequest{WorkerNamespace: "workers", WorkerPod: "worker", WorkerPodUid: "worker-uid", ExpectedActorUid: "actor-uid", CertificateSigningRequest: csr, Purpose: ateapipb.ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL} + if !proto.Equal(identity.request, want) { + t.Fatalf("MintCert request = %+v, want %+v", identity.request, want) + } +} + +func workerContext(t *testing.T, podUID string) context.Context { + t.Helper() + cert := workerCertificate(t, podUID, "node") + return peer.NewContext(context.Background(), &peer.Peer{AuthInfo: credentials.TLSInfo{State: tls.ConnectionState{PeerCertificates: []*x509.Certificate{cert}}}}) +} + +func TestVerifyClientOnSameNode(t *testing.T) { + state := tls.ConnectionState{PeerCertificates: []*x509.Certificate{workerCertificate(t, "worker-uid", "node-a")}} + nodeA := &substratex509.PodIdentity{NodeName: "node-a", NodeUID: "node-uid"} + if err := verifyClientOnSameNode(nodeA)(state); err != nil { + t.Fatalf("same-node worker rejected: %v", err) + } + if err := verifyClientOnSameNode(&substratex509.PodIdentity{NodeName: "node-b", NodeUID: "node-uid"})(state); err == nil { + t.Fatal("cross-node worker accepted") + } + if err := verifyClientOnSameNode(&substratex509.PodIdentity{NodeName: "node-a", NodeUID: "replacement-node"})(state); err == nil { + t.Fatal("replacement node accepted") + } +} + +func workerCertificate(t *testing.T, podUID, nodeName string) *x509.Certificate { + t.Helper() + _, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + template := &x509.Certificate{SerialNumber: big.NewInt(1), NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(time.Hour)} + if err := substratex509.AddPodIdentityToCertificate(&substratex509.PodIdentity{ + Namespace: "workers", ServiceAccountName: "default", ServiceAccountUID: "sa-uid", + PodName: "worker", PodUID: podUID, NodeName: nodeName, NodeUID: "node-uid", + }, template); err != nil { + t.Fatal(err) + } + der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) + if err != nil { + t.Fatal(err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + return cert +} diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 95a4e16c4..a188521ab 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -34,6 +34,7 @@ import ( "cloud.google.com/go/storage" "github.com/agent-substrate/substrate/cmd/atelet/internal/ategcs" + "github.com/agent-substrate/substrate/internal/ateapiauth" "github.com/agent-substrate/substrate/internal/ateerrors" "github.com/agent-substrate/substrate/internal/ateinterceptors" "github.com/agent-substrate/substrate/internal/ateompath" @@ -43,7 +44,9 @@ import ( "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/serverboot" + "github.com/agent-substrate/substrate/internal/substratex509" "github.com/agent-substrate/substrate/internal/version" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/google/go-containerregistry/pkg/authn" @@ -72,6 +75,9 @@ var ( grpcServerCredBundle = pflag.String("grpc-server-cred-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "Credential bundle atelet presents as its gRPC serving certificate.") clientCACerts = pflag.String("client-ca-certs", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "CA bundle used to verify gRPC client certificates.") + 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.") gcpAuthForImagePulls = pflag.Bool("gcp-auth-for-image-pulls", true, "Use GCP application default credentials mechanism.") localhostRegistryReplacement = pflag.String("localhost-registry-replacement", "", "The replacement registry endpoint for localhost and/or loopback IP addresses, useful for local development. for example kind-registry:5000") @@ -199,6 +205,19 @@ func main() { wrappedGCS, imageCache, ) + dialOpts, err := ateapiauth.DialOptions(ateapiauth.ClientConfig{ + CAFile: *ateapiCAFile, + ServerName: *ateapiServerName, + ClientCredBundle: *grpcServerCredBundle, + }) + if err != nil { + serverboot.Fatal(ctx, "Failed to build ateapi client credentials", err) + } + ateapiConn, err := grpc.NewClient(*ateapiAddress, dialOpts...) + if err != nil { + serverboot.Fatal(ctx, "Failed to create ateapi client", err) + } + defer ateapiConn.Close() lis, err := net.Listen("tcp", ":"+strconv.Itoa(*port)) if err != nil { @@ -209,6 +228,39 @@ func main() { if err != nil { serverboot.Fatal(ctx, "Failed to build server TLS config", err) } + ateletCert, err := credbundle.Parse(*grpcServerCredBundle) + if err != nil { + serverboot.Fatal(ctx, "Failed to load atelet Pod identity", err) + } + ateletIdentity, err := substratex509.PodIdentityFromCertificate(ateletCert.Leaf) + if err != nil { + serverboot.Fatal(ctx, "Failed to load atelet Pod identity", err) + } + if ateletIdentity == nil { + serverboot.Fatal(ctx, "Failed to load atelet Pod identity", fmt.Errorf("credential bundle has no Pod identity")) + } + brokerTLS := tlsCfg.Clone() + brokerTLS.VerifyConnection = verifyClientOnSameNode(ateletIdentity) + if err := os.Remove(ateompath.CredentialBrokerSocket); err != nil && !errors.Is(err, os.ErrNotExist) { + serverboot.Fatal(ctx, "Failed to remove stale credential broker socket", err) + } + brokerLis, err := net.Listen("unix", ateompath.CredentialBrokerSocket) + if err != nil { + serverboot.Fatal(ctx, "Failed to listen for credential broker", err) + } + defer brokerLis.Close() + if err := os.Chmod(ateompath.CredentialBrokerSocket, 0o600); err != nil { + serverboot.Fatal(ctx, "Failed to restrict credential broker socket", err) + } + brokerServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(brokerTLS))) + ateletpb.RegisterCredentialBrokerServer(brokerServer, &credentialBroker{ + actorIdentityClient: ateapipb.NewActorIdentityClient(ateapiConn), + }) + go func() { + if err := brokerServer.Serve(brokerLis); err != nil { + serverboot.Fatal(ctx, "Failed to serve credential broker", err) + } + }() svr := grpc.NewServer( grpc.Creds(credentials.NewTLS(tlsCfg)), @@ -345,6 +397,7 @@ func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (resp * RuntimeAssetPaths: assetPaths, Spec: buildAteomWorkloadSpec(req.GetSpec()), ActorUid: actorUID, + EgressGateway: toAteomEgressGateway(req.GetEgressGateway()), }); err != nil { return nil, fmt.Errorf("while calling ateom.RunWorkload: %w", err) } @@ -720,6 +773,7 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) Spec: buildAteomWorkloadSpec(req.GetSpec()), Scope: toAteomSnapshotScope(req.GetScope()), ActorUid: req.GetActorUid(), + EgressGateway: toAteomEgressGateway(req.GetEgressGateway()), // Informational: for DATA_ON_GOLDEN the golden snapshot's files are // already staged into the restore dir by the combined download above; // ateom restores from the shared dir and never fetches this URI. @@ -990,6 +1044,13 @@ func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec) *ateompb.WorkloadSpec { return out } +func toAteomEgressGateway(gateway *ateletpb.EgressGateway) *ateompb.EgressGateway { + if gateway == nil { + return nil + } + return &ateompb.EgressGateway{Address: gateway.GetAddress()} +} + // toAteomReadyz converts an ateletpb readyz probe into the ateompb wire // type. Returns nil when the source is nil so containers without a probe // stay unchanged on the wire to ateom. diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 88058cffe..5e893b6b0 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -674,6 +674,17 @@ func TestBuildAteomWorkloadSpecForwardsDurableDirMounts(t *testing.T) { } } +func TestToAteomEgressGateway(t *testing.T) { + if got := toAteomEgressGateway(nil); got != nil { + t.Fatalf("toAteomEgressGateway(nil) = %v, want nil", got) + } + want := &ateompb.EgressGateway{Address: "egress.example:443"} + got := toAteomEgressGateway(&ateletpb.EgressGateway{Address: want.Address}) + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("toAteomEgressGateway mismatch (-want +got):\n%s", diff) + } +} + func TestIsTerminalFileErr(t *testing.T) { tests := []struct { name string diff --git a/cmd/ateom-gvisor/main.go b/cmd/ateom-gvisor/main.go index 752acf2d3..80cd2b793 100644 --- a/cmd/ateom-gvisor/main.go +++ b/cmd/ateom-gvisor/main.go @@ -24,9 +24,11 @@ import ( "net" "net/url" "os" + "slices" "sort" "strings" "sync" + "time" "cloud.google.com/go/compute/metadata" "github.com/agent-substrate/substrate/internal/actorlog" @@ -55,11 +57,11 @@ var ( // TODO(liorlieberman) have a sub package for all atunnel releated things like that atunnelListenAddress = pflag.String("atunnel-listen-address", "0.0.0.0:443", "Address for actor ingress HTTPS") - atunnelCredentialBundle = pflag.String("atunnel-credential-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "PEM credential bundle for actor ingress HTTPS") - atunnelTrustBundle = pflag.String("atunnel-trust-bundle", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "PEM trust bundle for actor ingress clients") + workerCredentialBundle = pflag.String("atunnel-credential-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "Worker Pod credential bundle used by atunnel for inbound serving and outbound mTLS") + podIdentityTrustBundle = pflag.String("atunnel-trust-bundle", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "Pod identity trust bundle used for router clients and the node-local atelet") atunnelClientIdentity = pflag.String("atunnel-client-identity", "spiffe://cluster.local/ns/ate-system/sa/atenet-router", "SPIFFE identity allowed to call actor ingress HTTPS") atunnelEgressListenAddress = pflag.String("atunnel-egress-listen-address", "0.0.0.0:15001", "Address for transparently intercepted actor egress TCP") - atunnelEgressTrustBundle = pflag.String("atunnel-egress-trust-bundle", "/run/servicedns.podcert.ate.dev/trust-bundle.pem", "PEM trust bundle for the egress gateway") + egressGatewayTrustBundle = pflag.String("atunnel-egress-trust-bundle", "/run/servicedns.podcert.ate.dev/trust-bundle.pem", "Service DNS trust bundle for the remote egress gateway") showVersion = pflag.Bool("version", false, "Print version and exit.") logLevelFlag = pflag.String("log-level", "info", "Minimum log level: debug, info, warn, or error.") @@ -159,12 +161,12 @@ func do(ctx context.Context) error { if err != nil { return fmt.Errorf("while parsing atunnel upstream: %w", err) } - atunnelServer, atunnelEgress, atunnelEgressPort, err := runAtunnel(ctx, upstream) + atunnelIngress, atunnelEgress, atunnelEgressPort, err := runAtunnel(ctx, upstream) if err != nil { return err } - ateomService := NewService(interiorNetNS, actorLogger, atunnelServer, atunnelEgress, atunnelEgressPort, *atunnelCredentialBundle, *atunnelEgressTrustBundle) + ateomService := NewService(interiorNetNS, actorLogger, atunnelIngress, atunnelEgress, atunnelEgressPort, *workerCredentialBundle, *podIdentityTrustBundle, *egressGatewayTrustBundle) svr := grpc.NewServer( grpc.StatsHandler(otelgrpc.NewServerHandler()), @@ -182,9 +184,9 @@ func do(ctx context.Context) error { } func runAtunnel(ctx context.Context, upstream *url.URL) (*atunnel.Server, *atunnel.Egress, uint16, error) { - atunnelServer, err := atunnel.NewServer(atunnel.Config{ - CredentialBundlePath: *atunnelCredentialBundle, - TrustBundlePath: *atunnelTrustBundle, + atunnelIngress, err := atunnel.NewServer(atunnel.Config{ + CredentialBundlePath: *workerCredentialBundle, + TrustBundlePath: *podIdentityTrustBundle, AllowedClientID: *atunnelClientIdentity, Upstream: upstream, }) @@ -196,7 +198,7 @@ func runAtunnel(ctx context.Context, upstream *url.URL) (*atunnel.Server, *atunn return nil, nil, 0, fmt.Errorf("while opening atunnel listener: %w", err) } go func() { - if err := atunnelServer.Serve(ctx, atunnelListener); err != nil { + if err := atunnelIngress.Serve(ctx, atunnelListener); err != nil { serverboot.Fatal(ctx, "Failed to serve actor ingress", err) } }() @@ -222,7 +224,7 @@ func runAtunnel(ctx context.Context, upstream *url.URL) (*atunnel.Server, *atunn } }() slog.InfoContext(ctx, "atunnel egress serving", slog.String("address", *atunnelEgressListenAddress)) - return atunnelServer, atunnelEgress, atunnelEgressPort, nil + return atunnelIngress, atunnelEgress, atunnelEgressPort, nil } // AteomService is a service for shepherding single microvm. @@ -233,29 +235,36 @@ type AteomService struct { // subcommands are probably not safe to call concurrently. lock sync.Mutex - interiorNetNS netns.NsHandle - actorLogger *actorlog.ActorLogger - atunnel *atunnel.Server - atunnelEgress *atunnel.Egress - // atunnelEgressPort is zero when tunneled egress is disabled. Otherwise, - // actor TCP connections are transparently redirected to this local port. - atunnelEgressPort uint16 - atunnelCredentialBundle string - atunnelEgressTrustBundle string + interiorNetNS netns.NsHandle + actorLogger *actorlog.ActorLogger + atunnelIngress *atunnel.Server + atunnelEgress *atunnel.Egress + + // atunnelEgressPort is the local atunnel listener used as the target of the + // actor network's transparent TCP redirect. + atunnelEgressPort uint16 + // workerCredentialBundlePath contains the worker Pod certificate and key. + // Atunnel uses it for ingress serving and authentication to the atelet broker. + workerCredentialBundlePath string + // podIdentityTrustBundlePath verifies the node-local atelet's Pod identity. + podIdentityTrustBundlePath string + // egressGatewayTrustBundlePath verifies the remote gateway's serving cert. + egressGatewayTrustBundlePath string } var _ ateompb.AteomServer = (*AteomService)(nil) // NewService creates a new AteomService. -func NewService(interiorNetNS netns.NsHandle, actorLogger *actorlog.ActorLogger, atunnelServer *atunnel.Server, atunnelEgress *atunnel.Egress, atunnelEgressPort uint16, credentialBundle, egressTrustBundle string) *AteomService { +func NewService(interiorNetNS netns.NsHandle, actorLogger *actorlog.ActorLogger, atunnelIngress *atunnel.Server, atunnelEgress *atunnel.Egress, atunnelEgressPort uint16, workerCredentialBundlePath, podIdentityTrustBundlePath, egressGatewayTrustBundlePath string) *AteomService { return &AteomService{ - interiorNetNS: interiorNetNS, - actorLogger: actorLogger, - atunnel: atunnelServer, - atunnelEgress: atunnelEgress, - atunnelEgressPort: atunnelEgressPort, - atunnelCredentialBundle: credentialBundle, - atunnelEgressTrustBundle: egressTrustBundle, + interiorNetNS: interiorNetNS, + actorLogger: actorLogger, + atunnelIngress: atunnelIngress, + atunnelEgress: atunnelEgress, + atunnelEgressPort: atunnelEgressPort, + workerCredentialBundlePath: workerCredentialBundlePath, + podIdentityTrustBundlePath: podIdentityTrustBundlePath, + egressGatewayTrustBundlePath: egressGatewayTrustBundlePath, } } @@ -274,15 +283,30 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload // * Correct runsc version is downloaded and placed on disk. // * All OCI bundles are set up, including for "pause" container. + egress, err := s.prepareActorEgress(ctx, req.GetActorUid(), req.GetEgressGateway()) + if err != nil { + return nil, err + } if err := ateomnet.SetupActorNetwork(ctx, ateomnet.NetworkConfig{ InteriorNetNS: s.interiorNetNS, DumpNetInfo: true, - EgressRedirectPort: s.egressRedirectPort(req.GetEgressGatewayAddress() != ""), + EgressRedirectPort: s.egressRedirectPort(req.GetEgressGateway() != nil), }); err != nil { return nil, fmt.Errorf("while setting up actor network: %w", err) } + rcmd := &runsc{ + path: req.GetRunscPath(), + actorUID: req.GetActorUid(), + } + var containersToDelete []string defer func() { if retErr != nil { + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + if err := s.deactivateActorNetworking(cleanupCtx); err != nil { + slog.WarnContext(cleanupCtx, "Failed to deactivate actor networking after Run failure", slog.Any("err", err)) + } + deleteContainers(cleanupCtx, rcmd, containersToDelete, "Run") // Detach any bundle rootfs overlays a partially-completed setup // mounted, mirroring the post-checkpoint cleanup — otherwise they // linger in this namespace until atelet wipes the bundle dirs. @@ -291,17 +315,12 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Run failure", "actorUID", req.GetActorUid(), "err", err) } - if err := ateomnet.CleanupActorNetwork(ctx, s.interiorNetNS); err != nil { - slog.WarnContext(ctx, "Failed to clean up actor network after Run failure", slog.Any("err", err)) + if err := ateomnet.CleanupActorNetwork(cleanupCtx, s.interiorNetNS); err != nil { + slog.WarnContext(cleanupCtx, "Failed to clean up actor network after Run failure", slog.Any("err", err)) } } }() - rcmd := &runsc{ - path: req.GetRunscPath(), - actorUID: req.GetActorUid(), - } - // Create and start pause container. The bundle rootfs is composed here — // an overlay of the node's cached image layers plus the bundle's private // upper — because mounting is ateom's job (atelet runs with no @@ -310,6 +329,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload if err := imagecache.SetupBundleRootfs(ateompath.OCIBundlePath(req.GetActorUid(), "pause")); err != nil { return nil, fmt.Errorf("while composing pause rootfs: %w", err) } + containersToDelete = append(containersToDelete, "pause") if err := rcmd.cmdCreate(ctx, os.Stdout, "pause", nil); err != nil { return nil, fmt.Errorf("while creating pause container: %w", err) } @@ -328,6 +348,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload if err := imagecache.SetupBundleRootfs(ateompath.OCIBundlePath(req.GetActorUid(), ac.GetName())); err != nil { return nil, fmt.Errorf("while composing %q rootfs: %w", ac.GetName(), err) } + containersToDelete = append(containersToDelete, ac.GetName()) if err := rcmd.cmdCreate(ctx, pw, ac.GetName(), nil); err != nil { return nil, fmt.Errorf("while creating %q application container: %w", ac.GetName(), err) } @@ -340,7 +361,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload if err := readyz.WaitAll(ctx, req.GetSpec().GetContainers(), ateomnet.ActorVethIP); err != nil { return nil, fmt.Errorf("while waiting for container readyz: %w", err) } - if err := s.activateActorNetworking(req.GetAtespace(), req.GetActorName(), req.GetActorVersion(), req.GetEgressGatewayAddress()); err != nil { + if err := s.activateActorNetworking(req.GetAtespace(), req.GetActorName(), egress); err != nil { return nil, err } @@ -495,31 +516,41 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore // * All OCI bundles are set up, including for "pause" container. // * Checkpoint downloaded and placed on disk + egress, err := s.prepareActorEgress(ctx, req.GetActorUid(), req.GetEgressGateway()) + if err != nil { + return nil, err + } if err := ateomnet.SetupActorNetwork(ctx, ateomnet.NetworkConfig{ InteriorNetNS: s.interiorNetNS, DumpNetInfo: true, - EgressRedirectPort: s.egressRedirectPort(req.GetEgressGatewayAddress() != ""), + EgressRedirectPort: s.egressRedirectPort(req.GetEgressGateway() != nil), }); err != nil { return nil, fmt.Errorf("while setting up actor network: %w", err) } + rcmd := &runsc{ + path: req.GetRunscPath(), + actorUID: req.GetActorUid(), + } + var containersToDelete []string defer func() { if retErr != nil { + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + if err := s.deactivateActorNetworking(cleanupCtx); err != nil { + slog.WarnContext(cleanupCtx, "Failed to deactivate actor networking after Restore failure", slog.Any("err", err)) + } + deleteContainers(cleanupCtx, rcmd, containersToDelete, "Restore") // Same overlay detach as the Run-failure path above. if err := imagecache.UnmountAllUnder(ateompath.OCIBundleDir(req.GetActorUid())); err != nil { slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Restore failure", "actorUID", req.GetActorUid(), "err", err) } - if err := ateomnet.CleanupActorNetwork(ctx, s.interiorNetNS); err != nil { - slog.WarnContext(ctx, "Failed to clean up actor network after Restore failure", slog.Any("err", err)) + if err := ateomnet.CleanupActorNetwork(cleanupCtx, s.interiorNetNS); err != nil { + slog.WarnContext(cleanupCtx, "Failed to clean up actor network after Restore failure", slog.Any("err", err)) } } }() - rcmd := &runsc{ - path: req.GetRunscPath(), - actorUID: req.GetActorUid(), - } - checkpointDir := ateompath.RestoreStateDir(req.GetActorUid()) // Compose the pause rootfs before create (see RunWorkload). runsc restore @@ -532,6 +563,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore switch req.GetScope() { case ateompb.SnapshotScope_SNAPSHOT_SCOPE_DATA: // Create and restore pause container + containersToDelete = append(containersToDelete, "pause") if err := rcmd.cmdCreate(ctx, os.Stdout, "pause", []string{"--fs-restore-image-path", checkpointDir}); err != nil { return nil, fmt.Errorf("while creating pause container: %w", err) } @@ -540,6 +572,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore } case ateompb.SnapshotScope_SNAPSHOT_SCOPE_FULL: // Create and restore pause container + containersToDelete = append(containersToDelete, "pause") if err := rcmd.cmdCreate(ctx, os.Stdout, "pause", nil); err != nil { return nil, fmt.Errorf("while creating pause container: %w", err) } @@ -563,6 +596,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore } switch req.GetScope() { case ateompb.SnapshotScope_SNAPSHOT_SCOPE_DATA: + containersToDelete = append(containersToDelete, ac.GetName()) if err := rcmd.cmdCreate(ctx, pw, ac.GetName(), nil); err != nil { return nil, fmt.Errorf("while creating %q application container: %w", ac.GetName(), err) } @@ -570,6 +604,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore return nil, fmt.Errorf("while starting %q application container: %w", ac.GetName(), err) } case ateompb.SnapshotScope_SNAPSHOT_SCOPE_FULL: + containersToDelete = append(containersToDelete, ac.GetName()) if err := rcmd.cmdCreate(ctx, pw, ac.GetName(), nil); err != nil { return nil, fmt.Errorf("while creating %q application container: %w", ac.GetName(), err) } @@ -585,7 +620,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore if err := readyz.WaitAll(ctx, req.GetSpec().GetContainers(), ateomnet.ActorVethIP); err != nil { return nil, fmt.Errorf("while waiting for container readyz: %w", err) } - if err := s.activateActorNetworking(req.GetAtespace(), req.GetActorName(), req.GetActorVersion(), req.GetEgressGatewayAddress()); err != nil { + if err := s.activateActorNetworking(req.GetAtespace(), req.GetActorName(), egress); err != nil { return nil, err } @@ -594,49 +629,78 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore return &ateompb.RestoreWorkloadResponse{}, nil } -func (s *AteomService) activateActorNetworking(atespace, actorName string, actorVersion int64, egressGatewayAddress string) error { - var egressClient atunnel.EgressDialer - if s.atunnelEgress != nil && egressGatewayAddress != "" { - serverName, _, err := net.SplitHostPort(egressGatewayAddress) - if err != nil { - return fmt.Errorf("invalid egress gateway address %q: %w", egressGatewayAddress, err) - } - egressClient, err = atunnel.NewClient(atunnel.ClientConfig{ - GatewayAddress: egressGatewayAddress, - ServerName: serverName, - CredentialBundlePath: s.atunnelCredentialBundle, - TrustBundlePath: s.atunnelEgressTrustBundle, - }) - if err != nil { - return fmt.Errorf("while configuring actor egress client: %w", err) - } +type actorEgress struct { + // client presents the actor certificate to the remote egress gateway. + client *atunnel.Client + // certificateSource owns the actor key and renews its certificate via atelet. + certificateSource *atunnel.BrokerCertificateSource + expiresAt time.Time +} + +func (s *AteomService) prepareActorEgress(ctx context.Context, actorUID string, gateway *ateompb.EgressGateway) (*actorEgress, error) { + if gateway == nil { + return nil, nil } - if s.atunnel != nil { - if err := s.atunnel.Activate(atespace, actorName); err != nil { - return fmt.Errorf("while activating actor ingress: %w", err) - } + if gateway.GetAddress() == "" { + return nil, fmt.Errorf("egress gateway address is required") } - if egressClient != nil { - if err := s.atunnelEgress.Activate(egressClient, atespace, actorName, actorVersion, ""); err != nil { - if s.atunnel != nil { - _ = s.atunnel.Deactivate(context.Background()) - } - return fmt.Errorf("while activating actor egress: %w", err) - } + serverName, _, err := net.SplitHostPort(gateway.GetAddress()) + if err != nil { + return nil, fmt.Errorf("invalid egress gateway address %q: %w", gateway.GetAddress(), err) + } + certificateSource, err := atunnel.NewBrokerCertificateSource(atunnel.BrokerConfig{ + SocketPath: ateompath.CredentialBrokerSocket, + CredentialBundlePath: s.workerCredentialBundlePath, + TrustBundlePath: s.podIdentityTrustBundlePath, + ExpectedActorUID: actorUID, + }) + if err != nil { + return nil, fmt.Errorf("while configuring actor certificate broker: %w", err) + } + // Mint before starting the workload so configured tunneled egress fails + // closed. The source retains the private key for mTLS and renewal. + expiresAt, err := certificateSource.Mint(ctx) + if err != nil { + return nil, fmt.Errorf("while obtaining actor certificate: %w", err) + } + gatewayClient, err := atunnel.NewClient(atunnel.ClientConfig{ + GatewayAddress: gateway.GetAddress(), + ServerName: serverName, + GetClientCertificate: certificateSource.GetClientCertificate, + TrustBundlePath: s.egressGatewayTrustBundlePath, + }) + if err != nil { + return nil, fmt.Errorf("while configuring actor egress client: %w", err) + } + return &actorEgress{client: gatewayClient, certificateSource: certificateSource, expiresAt: expiresAt}, nil +} + +func (s *AteomService) activateActorNetworking(atespace, actorName string, egress *actorEgress) error { + if err := s.atunnelIngress.Activate(atespace, actorName); err != nil { + return fmt.Errorf("while activating actor ingress: %w", err) + } + if egress == nil { + return nil + } + if err := s.atunnelEgress.Activate(egress.client, egress.certificateSource, egress.expiresAt); err != nil { + return fmt.Errorf("while activating actor egress: %w", err) } return nil } +func deleteContainers(ctx context.Context, rcmd *runsc, containers []string, operation string) { + for _, container := range slices.Backward(containers) { + if err := rcmd.cmdDelete(ctx, container); err != nil { + slog.WarnContext(ctx, "Failed to delete runsc container after failure", + "operation", operation, "container", container, "err", err) + } + } +} + func (s *AteomService) deactivateActorNetworking(ctx context.Context) error { // Stop admitting traffic and drain active streams before the Actor network // is torn down. Attempt both directions even if one fails to deactivate. - var err error - if s.atunnel != nil { - err = errors.Join(err, s.atunnel.Deactivate(ctx)) - } - if s.atunnelEgress != nil { - err = errors.Join(err, s.atunnelEgress.Deactivate(ctx)) - } + err := errors.Join(s.atunnelIngress.Deactivate(ctx), s.atunnelEgress.Deactivate(ctx)) if err != nil { return fmt.Errorf("while deactivating actor networking: %w", err) } diff --git a/cmd/ateom-microvm/main.go b/cmd/ateom-microvm/main.go index 057863ac8..d0fe13029 100644 --- a/cmd/ateom-microvm/main.go +++ b/cmd/ateom-microvm/main.go @@ -33,6 +33,7 @@ import ( "os" "strings" "sync" + "time" "cloud.google.com/go/compute/metadata" "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/reaper" @@ -60,11 +61,11 @@ var ( logLevelFlag = flag.String("log-level", "info", "Minimum log level: debug, info, warn, or error.") atunnelListenAddress = flag.String("atunnel-listen-address", "0.0.0.0:443", "Address for actor ingress HTTPS") - atunnelCredentialBundle = flag.String("atunnel-credential-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "PEM credential bundle for actor ingress HTTPS") - atunnelTrustBundle = flag.String("atunnel-trust-bundle", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "PEM trust bundle for actor ingress clients") + workerCredentialBundle = flag.String("atunnel-credential-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "Worker Pod credential bundle used by atunnel for inbound serving and outbound mTLS") + podIdentityTrustBundle = flag.String("atunnel-trust-bundle", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "Pod identity trust bundle used for router clients and the node-local atelet") atunnelClientIdentity = flag.String("atunnel-client-identity", "spiffe://cluster.local/ns/ate-system/sa/atenet-router", "SPIFFE identity allowed to call actor ingress HTTPS") atunnelEgressListenAddress = flag.String("atunnel-egress-listen-address", "0.0.0.0:15001", "Address for transparently intercepted actor egress TCP") - atunnelEgressTrustBundle = flag.String("atunnel-egress-trust-bundle", "/run/servicedns.podcert.ate.dev/trust-bundle.pem", "PEM trust bundle for the egress gateway") + egressGatewayTrustBundle = flag.String("atunnel-egress-trust-bundle", "/run/servicedns.podcert.ate.dev/trust-bundle.pem", "Service DNS trust bundle for the remote egress gateway") ) const ( @@ -167,9 +168,9 @@ func do(ctx context.Context) error { if err != nil { return fmt.Errorf("while parsing atunnel upstream: %w", err) } - atunnelServer, err := atunnel.NewServer(atunnel.Config{ - CredentialBundlePath: *atunnelCredentialBundle, - TrustBundlePath: *atunnelTrustBundle, + atunnelIngress, err := atunnel.NewServer(atunnel.Config{ + CredentialBundlePath: *workerCredentialBundle, + TrustBundlePath: *podIdentityTrustBundle, AllowedClientID: *atunnelClientIdentity, Upstream: upstream, }) @@ -181,7 +182,7 @@ func do(ctx context.Context) error { return fmt.Errorf("while opening atunnel listener: %w", err) } go func() { - if err := atunnelServer.Serve(ctx, atunnelListener); err != nil { + if err := atunnelIngress.Serve(ctx, atunnelListener); err != nil { serverboot.Fatal(ctx, "Failed to serve actor ingress", err) } }() @@ -211,7 +212,7 @@ func do(ctx context.Context) error { grpc.StatsHandler(otelgrpc.NewServerHandler()), grpc.UnaryInterceptor(ateinterceptors.InternalServerUnaryInterceptor), ) - ateompb.RegisterAteomServer(svr, NewService(*podUID, *chBinary, *kataConfig, *kataDebug, interiorNetNS, actorLogger, atunnelServer, atunnelEgress, atunnelEgressPort, *atunnelCredentialBundle, *atunnelEgressTrustBundle)) + ateompb.RegisterAteomServer(svr, NewService(*podUID, *chBinary, *kataConfig, *kataDebug, interiorNetNS, actorLogger, atunnelIngress, atunnelEgress, atunnelEgressPort, *workerCredentialBundle, *podIdentityTrustBundle, *egressGatewayTrustBundle)) reflection.Register(svr) slog.InfoContext(ctx, "ateom-microvm serving", slog.String("socket", sockPath)) @@ -269,14 +270,20 @@ type AteomService struct { // actorLogger forwards the actor container's stdout/stderr to the worker pod's // stdout as ate.dev/*-labeled JSON and emits actor lifecycle events (parity // with ateom-gvisor). - actorLogger *actorlog.ActorLogger - atunnel *atunnel.Server - atunnelEgress *atunnel.Egress - // atunnelEgressPort is zero when tunneled egress is disabled. Otherwise, - // actor TCP connections are transparently redirected to this local port. - atunnelEgressPort uint16 - atunnelCredentialBundle string - atunnelEgressTrustBundle string + actorLogger *actorlog.ActorLogger + atunnelIngress *atunnel.Server + atunnelEgress *atunnel.Egress + + // atunnelEgressPort is the local atunnel listener used as the target of the + // actor network's transparent TCP redirect. + atunnelEgressPort uint16 + // workerCredentialBundlePath contains the worker Pod certificate and key. + // Atunnel uses it for ingress serving and authentication to the atelet broker. + workerCredentialBundlePath string + // podIdentityTrustBundlePath verifies the node-local atelet's Pod identity. + podIdentityTrustBundlePath string + // egressGatewayTrustBundlePath verifies the remote gateway's serving cert. + egressGatewayTrustBundlePath string // running maps actor UID -> the live micro-VM, kept so CheckpointWorkload can // pause+snapshot+teardown the same sandbox (and RestoreWorkload can track the @@ -287,52 +294,79 @@ type AteomService struct { var _ ateompb.AteomServer = (*AteomService)(nil) // NewService creates a new AteomService. -func NewService(podUID, chBinary, kataConfig string, kataDebug bool, interiorNetNS netns.NsHandle, actorLogger *actorlog.ActorLogger, atunnelServer *atunnel.Server, atunnelEgress *atunnel.Egress, atunnelEgressPort uint16, credentialBundle, egressTrustBundle string) *AteomService { +func NewService(podUID, chBinary, kataConfig string, kataDebug bool, interiorNetNS netns.NsHandle, actorLogger *actorlog.ActorLogger, atunnelIngress *atunnel.Server, atunnelEgress *atunnel.Egress, atunnelEgressPort uint16, workerCredentialBundlePath, podIdentityTrustBundlePath, egressGatewayTrustBundlePath string) *AteomService { return &AteomService{ - podUID: podUID, - chBinary: chBinary, - kataConfig: kataConfig, - kataDebug: kataDebug, - interiorNetNS: interiorNetNS, - actorLogger: actorLogger, - atunnel: atunnelServer, - atunnelEgress: atunnelEgress, - atunnelEgressPort: atunnelEgressPort, - atunnelCredentialBundle: credentialBundle, - atunnelEgressTrustBundle: egressTrustBundle, - running: map[string]*runningActor{}, + podUID: podUID, + chBinary: chBinary, + kataConfig: kataConfig, + kataDebug: kataDebug, + interiorNetNS: interiorNetNS, + actorLogger: actorLogger, + atunnelIngress: atunnelIngress, + atunnelEgress: atunnelEgress, + atunnelEgressPort: atunnelEgressPort, + workerCredentialBundlePath: workerCredentialBundlePath, + podIdentityTrustBundlePath: podIdentityTrustBundlePath, + egressGatewayTrustBundlePath: egressGatewayTrustBundlePath, + running: map[string]*runningActor{}, } } -func (s *AteomService) activateActorNetworking(atespace, actorName string, actorVersion int64, egressGatewayAddress string) error { - var egressClient atunnel.EgressDialer - if s.atunnelEgress != nil && egressGatewayAddress != "" { - serverName, _, err := net.SplitHostPort(egressGatewayAddress) - if err != nil { - return fmt.Errorf("invalid egress gateway address %q: %w", egressGatewayAddress, err) - } - egressClient, err = atunnel.NewClient(atunnel.ClientConfig{ - GatewayAddress: egressGatewayAddress, - ServerName: serverName, - CredentialBundlePath: s.atunnelCredentialBundle, - TrustBundlePath: s.atunnelEgressTrustBundle, - }) - if err != nil { - return fmt.Errorf("while configuring actor egress client: %w", err) - } +type actorEgress struct { + // client presents the actor certificate to the remote egress gateway. + client *atunnel.Client + // certificateSource owns the actor key and renews its certificate via atelet. + certificateSource *atunnel.BrokerCertificateSource + expiresAt time.Time +} + +func (s *AteomService) prepareActorEgress(ctx context.Context, actorUID string, gateway *ateompb.EgressGateway) (*actorEgress, error) { + if gateway == nil { + return nil, nil } - if s.atunnel != nil { - if err := s.atunnel.Activate(atespace, actorName); err != nil { - return fmt.Errorf("while activating actor ingress: %w", err) - } + if gateway.GetAddress() == "" { + return nil, fmt.Errorf("egress gateway address is required") } - if egressClient != nil { - if err := s.atunnelEgress.Activate(egressClient, atespace, actorName, actorVersion, ""); err != nil { - if s.atunnel != nil { - _ = s.atunnel.Deactivate(context.Background()) - } - return fmt.Errorf("while activating actor egress: %w", err) - } + serverName, _, err := net.SplitHostPort(gateway.GetAddress()) + if err != nil { + return nil, fmt.Errorf("invalid egress gateway address %q: %w", gateway.GetAddress(), err) + } + certificateSource, err := atunnel.NewBrokerCertificateSource(atunnel.BrokerConfig{ + SocketPath: ateompath.CredentialBrokerSocket, + CredentialBundlePath: s.workerCredentialBundlePath, + TrustBundlePath: s.podIdentityTrustBundlePath, + ExpectedActorUID: actorUID, + }) + if err != nil { + return nil, fmt.Errorf("while configuring actor certificate broker: %w", err) + } + // Mint before starting the workload so configured tunneled egress fails + // closed. The source retains the private key for mTLS and renewal. + expiresAt, err := certificateSource.Mint(ctx) + if err != nil { + return nil, fmt.Errorf("while obtaining actor certificate: %w", err) + } + gatewayClient, err := atunnel.NewClient(atunnel.ClientConfig{ + GatewayAddress: gateway.GetAddress(), + ServerName: serverName, + GetClientCertificate: certificateSource.GetClientCertificate, + TrustBundlePath: s.egressGatewayTrustBundlePath, + }) + if err != nil { + return nil, fmt.Errorf("while configuring actor egress client: %w", err) + } + return &actorEgress{client: gatewayClient, certificateSource: certificateSource, expiresAt: expiresAt}, nil +} + +func (s *AteomService) activateActorNetworking(atespace, actorName string, egress *actorEgress) error { + if err := s.atunnelIngress.Activate(atespace, actorName); err != nil { + return fmt.Errorf("while activating actor ingress: %w", err) + } + if egress == nil { + return nil + } + if err := s.atunnelEgress.Activate(egress.client, egress.certificateSource, egress.expiresAt); err != nil { + return fmt.Errorf("while activating actor egress: %w", err) } return nil } @@ -340,13 +374,7 @@ func (s *AteomService) activateActorNetworking(atespace, actorName string, actor func (s *AteomService) deactivateActorNetworking(ctx context.Context) error { // Stop admitting traffic and drain active streams before the Actor network // is torn down. Attempt both directions even if one fails to deactivate. - var err error - if s.atunnel != nil { - err = errors.Join(err, s.atunnel.Deactivate(ctx)) - } - if s.atunnelEgress != nil { - err = errors.Join(err, s.atunnelEgress.Deactivate(ctx)) - } + err := errors.Join(s.atunnelIngress.Deactivate(ctx), s.atunnelEgress.Deactivate(ctx)) if err != nil { return fmt.Errorf("while deactivating actor networking: %w", err) } diff --git a/cmd/ateom-microvm/restore.go b/cmd/ateom-microvm/restore.go index a0c76affc..3a9f58a0d 100644 --- a/cmd/ateom-microvm/restore.go +++ b/cmd/ateom-microvm/restore.go @@ -70,8 +70,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore containers: req.GetSpec().GetContainers(), assetPaths: req.GetRuntimeAssetPaths(), - actorVersion: req.GetActorVersion(), - egressGatewayAddress: req.GetEgressGatewayAddress(), + egressGateway: req.GetEgressGateway(), } restoreDir := ateompath.RestoreStateDir(p.actorUID) durableDir := ateompath.DurableDirVolumeMountsDir(p.actorUID) @@ -133,6 +132,10 @@ func (s *AteomService) restoreFullScope(ctx context.Context, p actorBootParams, templateNS, templateName := p.templateNS, p.templateName rr := s.resolveRuntime(p.assetPaths) + egress, err := s.prepareActorEgress(ctx, p.actorUID, p.egressGateway) + if err != nil { + return err + } kata.CleanupSandboxState(ctx, actorUID) // Repoint the snapshot's vsock socket to this actor's VMDir (the disk + kernel @@ -203,14 +206,19 @@ func (s *AteomService) restoreFullScope(ctx context.Context, p actorBootParams, InteriorNetNS: s.interiorNetNS, HostVethHWAddr: hostVethHWAddr, SweepInteriorLinks: true, - EgressRedirectPort: s.egressRedirectPort(p.egressGatewayAddress != ""), + EgressRedirectPort: s.egressRedirectPort(p.egressGateway != nil), }); err != nil { return fmt.Errorf("while setting up actor network: %w", err) } defer func() { if retErr != nil { - if cleanupErr := ateomnet.CleanupActorNetwork(ctx, s.interiorNetNS); cleanupErr != nil { - slog.WarnContext(ctx, "Failed to clean up actor network after Restore failure", slog.Any("err", cleanupErr)) + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + if cleanupErr := s.deactivateActorNetworking(cleanupCtx); cleanupErr != nil { + slog.WarnContext(cleanupCtx, "Failed to deactivate actor networking after Restore failure", slog.Any("err", cleanupErr)) + } + if cleanupErr := ateomnet.CleanupActorNetwork(cleanupCtx, s.interiorNetNS); cleanupErr != nil { + slog.WarnContext(cleanupCtx, "Failed to clean up actor network after Restore failure", slog.Any("err", cleanupErr)) } // Detach any bundle rootfs overlays mounted by buildActorContainers // before the failure, mirroring teardownActor's cleanup. @@ -298,7 +306,7 @@ func (s *AteomService) restoreFullScope(ctx context.Context, p actorBootParams, } } - if err := s.activateActorNetworking(p.actorRef.Atespace, p.actorRef.Name, p.actorVersion, p.egressGatewayAddress); err != nil { + if err := s.activateActorNetworking(p.actorRef.Atespace, p.actorRef.Name, egress); err != nil { return err } s.running[actorUID] = ra diff --git a/cmd/ateom-microvm/run.go b/cmd/ateom-microvm/run.go index ae691a2ca..8d6f61df2 100644 --- a/cmd/ateom-microvm/run.go +++ b/cmd/ateom-microvm/run.go @@ -212,8 +212,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload containers: req.GetSpec().GetContainers(), assetPaths: req.GetRuntimeAssetPaths(), - actorVersion: req.GetActorVersion(), - egressGatewayAddress: req.GetEgressGatewayAddress(), + egressGateway: req.GetEgressGateway(), } s.actorLogger.EmitLifecycleLog("Actor starting", p.actorRef, p.actorUID, p.templateNS, p.templateName) @@ -235,12 +234,8 @@ type actorBootParams struct { templateName string containers []*ateompb.Container assetPaths map[string]string - // actorVersion is the Actor resource version ate-api observed when it - // assigned this worker; atunnel asserts it to the egress gateway. - actorVersion int64 - // egressGatewayAddress is empty unless an egress gateway is configured, in - // which case actor TCP egress is redirected to atunnel's local listener. - egressGatewayAddress string + // egressGateway is nil unless actor TCP should be redirected through atunnel. + egressGateway *ateompb.EgressGateway } // coldBootAttempts is how many times a cold boot is tried when the micro-VM @@ -298,6 +293,10 @@ func (s *AteomService) coldBootActor(ctx context.Context, p actorBootParams) (re return fmt.Errorf("ateom-microvm requires %q and %q asset paths", assetKernel, assetImage) } rr := s.resolveRuntime(paths) + egress, err := s.prepareActorEgress(ctx, p.actorUID, p.egressGateway) + if err != nil { + return err + } // Networking (host side): per-activation veth into the interior netns. The // tap + TC mirror is built below (after the VM exists) so its FDs are fresh. @@ -305,14 +304,19 @@ func (s *AteomService) coldBootActor(ctx context.Context, p actorBootParams) (re InteriorNetNS: s.interiorNetNS, HostVethHWAddr: hostVethHWAddr, SweepInteriorLinks: true, - EgressRedirectPort: s.egressRedirectPort(p.egressGatewayAddress != ""), + EgressRedirectPort: s.egressRedirectPort(p.egressGateway != nil), }); err != nil { return fmt.Errorf("while setting up actor network: %w", err) } defer func() { if retErr != nil { - if cleanupErr := ateomnet.CleanupActorNetwork(ctx, s.interiorNetNS); cleanupErr != nil { - slog.WarnContext(ctx, "Failed to clean up actor network after Run failure", slog.Any("err", cleanupErr)) + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + if cleanupErr := s.deactivateActorNetworking(cleanupCtx); cleanupErr != nil { + slog.WarnContext(cleanupCtx, "Failed to deactivate actor networking after Run failure", slog.Any("err", cleanupErr)) + } + if cleanupErr := ateomnet.CleanupActorNetwork(cleanupCtx, s.interiorNetNS); cleanupErr != nil { + slog.WarnContext(cleanupCtx, "Failed to clean up actor network after Run failure", slog.Any("err", cleanupErr)) } // Detach any bundle rootfs overlays mounted by buildActorContainers // before the failure, mirroring teardownActor's cleanup. @@ -459,7 +463,7 @@ func (s *AteomService) coldBootActor(ctx context.Context, p actorBootParams) (re } ra := &runningActor{chCmd: chCmd, vfsdCmd: vfsdCmd, durableVfsdCmd: durableVfsdCmd, apiSocket: apiSocket, baseID: actorUID, logAgent: ac} - if err := s.activateActorNetworking(p.actorRef.Atespace, p.actorRef.Name, p.actorVersion, p.egressGatewayAddress); err != nil { + if err := s.activateActorNetworking(p.actorRef.Atespace, p.actorRef.Name, egress); err != nil { return err } s.running[actorUID] = ra diff --git a/internal/ateompath/ateompath.go b/internal/ateompath/ateompath.go index ad47c7f85..a0ef21168 100644 --- a/internal/ateompath/ateompath.go +++ b/internal/ateompath/ateompath.go @@ -34,6 +34,10 @@ var ( // directories are visible at the same path in atelet (which writes them) // and in every ateom pod (which mounts them as overlay lowerdirs). ImageCacheDir = filepath.Join(BasePath, "image-cache") + + // CredentialBrokerSocket is the node-local atelet socket used by atunnel + // to request credentials for the worker's current actor assignment. + CredentialBrokerSocket = filepath.Join(BasePath, "credential-broker.sock") ) func RunSCBinaryPath(sha256 string) string { diff --git a/internal/atunnel/client.go b/internal/atunnel/client.go index 1a7817a5e..666431bbf 100644 --- a/internal/atunnel/client.go +++ b/internal/atunnel/client.go @@ -27,20 +27,6 @@ import ( "os" "strconv" "strings" - - "github.com/agent-substrate/substrate/internal/resources" -) - -const ( - // ActorAtespaceHeader identifies the atespace whose actor opened an egress - // tunnel. The egress gateway must authenticate this metadata before using it - // for policy decisions. - ActorAtespaceHeader = "X-Ate-Atespace" - // ActorNameHeader identifies the actor that opened an egress tunnel. - ActorNameHeader = "X-Ate-Actor-Name" - // ActorVersionHeader is the Actor resource version observed when the worker - // was assigned. Gateways use it as a lower bound on cached Actor metadata. - ActorVersionHeader = "X-Ate-Actor-Version" ) // TODO(liorlieberman): support/use CONNECT on Ingress as well. @@ -48,7 +34,7 @@ const ( type ClientConfig struct { GatewayAddress string ServerName string - CredentialBundlePath string + GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error) TrustBundlePath string } @@ -69,15 +55,6 @@ func WithDialer(dial DialFunc) ClientOption { } } -// EgressMetadata is attached to an egress CONNECT request. BearerToken is -// optional until actor JWT issuance is wired into ateom. -type EgressMetadata struct { - Atespace string - ActorName string - ActorVersion int64 - BearerToken string -} - // Client opens actor egress streams through an mTLS-authenticated gateway. type Client struct { gatewayAddress string @@ -85,8 +62,8 @@ type Client struct { dialContext DialFunc } -// Client implements EgressDialer. -var _ EgressDialer = (*Client)(nil) +// Client implements egressDialer. +var _ egressDialer = (*Client)(nil) // NewClient creates an egress CONNECT client and validates its TLS material. func NewClient(cfg ClientConfig, opts ...ClientOption) (*Client, error) { @@ -96,15 +73,12 @@ func NewClient(cfg ClientConfig, opts ...ClientOption) (*Client, error) { if cfg.ServerName == "" { return nil, fmt.Errorf("atunnel: egress gateway server name is required") } - if cfg.CredentialBundlePath == "" { - return nil, fmt.Errorf("atunnel: credential bundle path is required") + if cfg.GetClientCertificate == nil { + return nil, fmt.Errorf("atunnel: client certificate source is required") } if cfg.TrustBundlePath == "" { return nil, fmt.Errorf("atunnel: trust bundle path is required") } - if _, err := loadCredentialBundle(cfg.CredentialBundlePath); err != nil { - return nil, err - } trustPEM, err := os.ReadFile(cfg.TrustBundlePath) if err != nil { return nil, fmt.Errorf("atunnel: reading trust bundle: %w", err) @@ -114,17 +88,14 @@ func NewClient(cfg ClientConfig, opts ...ClientOption) (*Client, error) { return nil, fmt.Errorf("atunnel: trust bundle %q contains no certificates", cfg.TrustBundlePath) } - credentialBundlePath := cfg.CredentialBundlePath client := &Client{ gatewayAddress: cfg.GatewayAddress, dialContext: (&net.Dialer{}).DialContext, tlsConfig: &tls.Config{ - MinVersion: tls.VersionTLS12, - RootCAs: rootCAs, - ServerName: cfg.ServerName, - GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { - return loadCredentialBundle(credentialBundlePath) - }, + MinVersion: tls.VersionTLS12, + RootCAs: rootCAs, + ServerName: cfg.ServerName, + GetClientCertificate: cfg.GetClientCertificate, }, } for _, opt := range opts { @@ -135,17 +106,10 @@ func NewClient(cfg ClientConfig, opts ...ClientOption) (*Client, error) { // DialContext opens a CONNECT tunnel to destination. destination becomes the // request authority, so it must include an explicit port. -func (c *Client) DialContext(ctx context.Context, destination string, metadata EgressMetadata) (net.Conn, error) { +func (c *Client) DialContext(ctx context.Context, destination string) (net.Conn, error) { if err := validateDestination(destination); err != nil { return nil, err } - if !resources.IsValidResourceName(metadata.Atespace) || !resources.IsValidResourceName(metadata.ActorName) { - return nil, fmt.Errorf("atunnel: invalid actor identity %q/%q", metadata.Atespace, metadata.ActorName) - } - if metadata.ActorVersion < 1 { - return nil, fmt.Errorf("atunnel: actor version must be positive") - } - rawConn, err := c.dialContext(ctx, "tcp", c.gatewayAddress) if err != nil { return nil, fmt.Errorf("atunnel: connecting to egress gateway: %w", err) @@ -160,14 +124,6 @@ func (c *Client) DialContext(ctx context.Context, destination string, metadata E Method: http.MethodConnect, URL: &url.URL{Host: destination}, Host: destination, - Header: http.Header{ - ActorAtespaceHeader: []string{metadata.Atespace}, - ActorNameHeader: []string{metadata.ActorName}, - ActorVersionHeader: []string{strconv.FormatInt(metadata.ActorVersion, 10)}, - }, - } - if metadata.BearerToken != "" { - req.Header.Set("Authorization", "Bearer "+metadata.BearerToken) } if err := req.Write(tlsConn); err != nil { _ = tlsConn.Close() diff --git a/internal/atunnel/client_test.go b/internal/atunnel/client_test.go index 9d74884d4..4f9dc78e4 100644 --- a/internal/atunnel/client_test.go +++ b/internal/atunnel/client_test.go @@ -55,12 +55,7 @@ func TestClientDialContext(t *testing.T) { }) client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) - conn, err := client.DialContext(context.Background(), "192.0.2.10:443", EgressMetadata{ - Atespace: "team-a", - ActorName: "actor-1", - ActorVersion: 7, - BearerToken: "actor-token", - }) + conn, err := client.DialContext(context.Background(), "192.0.2.10:443") if err != nil { t.Fatal(err) } @@ -73,17 +68,13 @@ func TestClientDialContext(t *testing.T) { if gotRequest.Host != "192.0.2.10:443" { t.Errorf("authority = %q, want 192.0.2.10:443", gotRequest.Host) } - if got := gotRequest.Header.Get(ActorAtespaceHeader); got != "team-a" { - t.Errorf("%s = %q, want team-a", ActorAtespaceHeader, got) - } - if got := gotRequest.Header.Get(ActorNameHeader); got != "actor-1" { - t.Errorf("%s = %q, want actor-1", ActorNameHeader, got) - } - if got := gotRequest.Header.Get(ActorVersionHeader); got != "7" { - t.Errorf("%s = %q, want 7", ActorVersionHeader, got) + for name := range gotRequest.Header { + if strings.HasPrefix(strings.ToLower(name), "x-ate-") { + t.Errorf("legacy identity header %q was sent", name) + } } - if got := gotRequest.Header.Get("Authorization"); got != "Bearer actor-token" { - t.Errorf("Authorization = %q, want Bearer actor-token", got) + if got := gotRequest.Header.Get("Authorization"); got != "" { + t.Errorf("Authorization = %q, want empty", got) } buffered := make([]byte, len("hello")) @@ -106,11 +97,7 @@ func TestClientDialContextRejected(t *testing.T) { }) client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) - _, err := client.DialContext(context.Background(), "192.0.2.10:443", EgressMetadata{ - Atespace: "team-a", - ActorName: "actor-1", - ActorVersion: 7, - }) + _, err := client.DialContext(context.Background(), "192.0.2.10:443") if err == nil || !strings.Contains(err.Error(), "denied by policy") { t.Fatalf("DialContext error = %v, want policy rejection", err) } @@ -122,37 +109,19 @@ func TestClientDialContextValidatesInput(t *testing.T) { tests := []struct { name string destination string - metadata EgressMetadata }{ { name: "destination has no port", destination: "192.0.2.10", - metadata: EgressMetadata{Atespace: "team-a", ActorName: "actor-1", ActorVersion: 7}, }, { name: "destination is a hostname", destination: "example.com:443", - metadata: EgressMetadata{Atespace: "team-a", ActorName: "actor-1", ActorVersion: 7}, - }, - { - name: "invalid atespace", - destination: "192.0.2.10:443", - metadata: EgressMetadata{Atespace: "TEAM A", ActorName: "actor-1", ActorVersion: 7}, - }, - { - name: "invalid actor", - destination: "192.0.2.10:443", - metadata: EgressMetadata{Atespace: "team-a", ActorName: "actor/1", ActorVersion: 7}, - }, - { - name: "invalid actor version", - destination: "192.0.2.10:443", - metadata: EgressMetadata{Atespace: "team-a", ActorName: "actor-1"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if _, err := client.DialContext(context.Background(), tt.destination, tt.metadata); err == nil { + if _, err := client.DialContext(context.Background(), tt.destination); err == nil { t.Fatal("DialContext unexpectedly succeeded") } }) @@ -170,19 +139,18 @@ func dialFixedAddress(address string) DialFunc { func newTestClient(t *testing.T, ca *testCA, opts ...ClientOption) *Client { t.Helper() dir := t.TempDir() - bundlePath := filepath.Join(dir, "client.pem") trustPath := filepath.Join(dir, "trust.pem") - writeCredentialBundle(t, bundlePath, ca.issue(t, - "spiffe://cluster.local/ns/ate-demo/sa/ateom", + certificate := ca.issue(t, + "spiffe://substrate-actor.local/atespace/team/actor/actor", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - )) + ) if err := os.WriteFile(trustPath, ca.certPEM, 0o600); err != nil { t.Fatal(err) } client, err := NewClient(ClientConfig{ GatewayAddress: "127.0.0.1:1", ServerName: "egress.test", - CredentialBundlePath: bundlePath, + GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return &certificate, nil }, TrustBundlePath: trustPath, }, opts...) if err != nil { diff --git a/internal/atunnel/credential.go b/internal/atunnel/credential.go new file mode 100644 index 000000000..65090af76 --- /dev/null +++ b/internal/atunnel/credential.go @@ -0,0 +1,197 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package atunnel + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "net/url" + "os" + "path" + "slices" + "sync" + "time" + + "github.com/agent-substrate/substrate/internal/credbundle" + "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/substratex509" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// BrokerCertificateSource owns atunnel's actor private key and obtains the +// matching short-lived certificate from the node-local atelet. +type BrokerCertificateSource struct { + socketPath string + expectedActorUID string + tlsConfig *tls.Config + privateKey *ecdsa.PrivateKey + + mu sync.RWMutex + certificate *tls.Certificate +} + +// BrokerConfig configures the node-local atelet credential broker client. +type BrokerConfig struct { + // SocketPath is the atelet-owned Unix socket shared with this worker. + SocketPath string + // CredentialBundlePath is the worker Pod certificate and private key used + // only to authenticate atunnel to atelet. + CredentialBundlePath string + // TrustBundlePath verifies atelet's Pod certificate. + TrustBundlePath string + // ExpectedActorUID prevents a mint started for an old activation from + // receiving the newly assigned actor's certificate. + ExpectedActorUID string +} + +// NewBrokerCertificateSource creates one actor key for this activation. The key +// is reused across renewals and never leaves atunnel; only its CSR crosses the +// credential broker socket. +func NewBrokerCertificateSource(cfg BrokerConfig) (*BrokerCertificateSource, error) { + if cfg.SocketPath == "" || cfg.CredentialBundlePath == "" || cfg.TrustBundlePath == "" || cfg.ExpectedActorUID == "" { + return nil, fmt.Errorf("atunnel: credential broker socket, credentials, trust bundle, and expected actor UID are required") + } + localCert, err := credbundle.Parse(cfg.CredentialBundlePath) + if err != nil { + return nil, fmt.Errorf("atunnel: load worker identity: %w", err) + } + localIdentity, err := substratex509.PodIdentityFromCertificate(localCert.Leaf) + if err != nil || localIdentity == nil { + return nil, fmt.Errorf("atunnel: worker certificate has no valid Pod identity") + } + trustPEM, err := os.ReadFile(cfg.TrustBundlePath) + if err != nil { + return nil, fmt.Errorf("atunnel: read credential broker trust bundle: %w", err) + } + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(trustPEM) { + return nil, fmt.Errorf("atunnel: credential broker trust bundle contains no certificates") + } + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("atunnel: generate actor private key: %w", err) + } + expectedURI := (&url.URL{Scheme: "spiffe", Host: "cluster.local", Path: path.Join("ns", "ate-system", "sa", "atelet")}).String() + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS13, + InsecureSkipVerify: true, // Verification below supports SPIFFE Pod certificates without a DNS name. + GetClientCertificate: credbundle.ClientLoader(cfg.CredentialBundlePath), + VerifyConnection: func(state tls.ConnectionState) error { + // Verify both the normal server-auth chain and the identities that DNS + // verification cannot express: atelet's SPIFFE ID and exact node + // incarnation. This is why InsecureSkipVerify is set above. + if len(state.PeerCertificates) == 0 { + return fmt.Errorf("credential broker certificate is required") + } + intermediates := x509.NewCertPool() + for _, cert := range state.PeerCertificates[1:] { + intermediates.AddCert(cert) + } + if _, err := state.PeerCertificates[0].Verify(x509.VerifyOptions{Roots: roots, Intermediates: intermediates, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}); err != nil { + return fmt.Errorf("verify credential broker certificate: %w", err) + } + leaf := state.PeerCertificates[0] + if len(leaf.URIs) != 1 || leaf.URIs[0].String() != expectedURI { + return fmt.Errorf("credential broker is not atelet") + } + identity, err := substratex509.PodIdentityFromCertificate(leaf) + if err != nil || identity == nil || identity.NodeName != localIdentity.NodeName || identity.NodeUID != localIdentity.NodeUID { + return fmt.Errorf("credential broker is not on worker node %q (%s)", localIdentity.NodeName, localIdentity.NodeUID) + } + return nil + }, + } + + return &BrokerCertificateSource{socketPath: cfg.SocketPath, expectedActorUID: cfg.ExpectedActorUID, tlsConfig: tlsConfig, privateKey: privateKey}, nil +} + +// Mint requests and installs a fresh certificate for the source's existing +// actor key. It returns the new expiry for renewal scheduling. +func (s *BrokerCertificateSource) Mint(ctx context.Context) (time.Time, error) { + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, s.privateKey) + if err != nil { + return time.Time{}, fmt.Errorf("atunnel: create actor CSR: %w", err) + } + // A fresh connection picks up rotated worker credentials and forces atelet's + // current certificate and node identity to be verified for every mint. + conn, err := grpc.NewClient("passthrough:///credential-broker", + grpc.WithTransportCredentials(credentials.NewTLS(s.tlsConfig)), + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", s.socketPath) + }), + ) + if err != nil { + return time.Time{}, err + } + defer conn.Close() + resp, err := ateletpb.NewCredentialBrokerClient(conn).MintActorCertificate(ctx, &ateletpb.MintActorCertificateRequest{ + CertificateSigningRequest: csr, + ExpectedActorUid: s.expectedActorUID, + }) + if err != nil { + return time.Time{}, fmt.Errorf("atunnel: mint actor certificate: %w", err) + } + chain := resp.GetActorCertificates() + if len(chain) == 0 { + return time.Time{}, fmt.Errorf("atunnel: credential broker returned no actor certificate") + } + leaf, err := x509.ParseCertificate(chain[0]) + if err != nil { + return time.Time{}, fmt.Errorf("atunnel: parse actor certificate: %w", err) + } + if !s.privateKey.PublicKey.Equal(leaf.PublicKey) { + return time.Time{}, fmt.Errorf("atunnel: actor certificate does not match private key") + } + now := time.Now() + if now.Before(leaf.NotBefore) || !leaf.NotAfter.After(now) { + return time.Time{}, fmt.Errorf("atunnel: credential broker returned an invalid actor certificate lifetime") + } + if !slices.Contains(leaf.ExtKeyUsage, x509.ExtKeyUsageClientAuth) { + return time.Time{}, fmt.Errorf("atunnel: actor certificate cannot authenticate a TLS client") + } + identity, err := substratex509.ActorIdentityFromCertificate(leaf) + if err != nil || identity == nil { + return time.Time{}, fmt.Errorf("atunnel: actor certificate has no valid actor identity") + } + if identity.Purpose != substratex509.ActorIdentityPurposeAtunnel { + return time.Time{}, fmt.Errorf("atunnel: actor certificate is not scoped to atunnel") + } + if identity.ActorUid != s.expectedActorUID { + return time.Time{}, fmt.Errorf("atunnel: actor certificate is for an unexpected actor") + } + cert := &tls.Certificate{Certificate: chain, PrivateKey: s.privateKey, Leaf: leaf} + s.mu.Lock() + s.certificate = cert + s.mu.Unlock() + return leaf.NotAfter, nil +} + +// GetClientCertificate supplies the current actor certificate to the egress +// gateway TLS handshake and refuses to use it after expiry. +func (s *BrokerCertificateSource) GetClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + s.mu.RLock() + defer s.mu.RUnlock() + if s.certificate == nil || !s.certificate.Leaf.NotAfter.After(time.Now()) { + return nil, fmt.Errorf("atunnel: no valid actor certificate") + } + return s.certificate, nil +} diff --git a/internal/atunnel/credential_test.go b/internal/atunnel/credential_test.go new file mode 100644 index 000000000..02c3dff35 --- /dev/null +++ b/internal/atunnel/credential_test.go @@ -0,0 +1,223 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package atunnel + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/substratex509" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" +) + +func TestBrokerCertificateSourceMintsAndReusesKey(t *testing.T) { + source, broker := newTestBrokerCertificateSource(t, testAteletIdentity("node-a"), time.Hour) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + for range 2 { + if _, err := source.Mint(ctx); err != nil { + t.Fatal(err) + } + } + first, second := <-broker.publicKeys, <-broker.publicKeys + if string(first) != string(second) { + t.Fatal("renewal replaced the actor private key") + } + cert, err := source.GetClientCertificate(nil) + if err != nil { + t.Fatal(err) + } + identity, err := substratex509.ActorIdentityFromCertificate(cert.Leaf) + if err != nil { + t.Fatal(err) + } + if identity == nil || identity.ActorUid != "actor-uid" || identity.Purpose != substratex509.ActorIdentityPurposeAtunnel { + t.Fatalf("actor identity = %+v", identity) + } +} + +func TestBrokerCertificateSourceRejectsAteletOnDifferentNode(t *testing.T) { + source, _ := newTestBrokerCertificateSource(t, testAteletIdentity("node-b"), time.Hour) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if _, err := source.Mint(ctx); err == nil || !strings.Contains(err.Error(), "not on worker node") { + t.Fatalf("Mint() error = %v, want node identity rejection", err) + } +} + +func TestBrokerCertificateSourceRejectsExpiredCertificate(t *testing.T) { + source, _ := newTestBrokerCertificateSource(t, testAteletIdentity("node-a"), -time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if _, err := source.Mint(ctx); err == nil || !strings.Contains(err.Error(), "invalid actor certificate lifetime") { + t.Fatalf("Mint() error = %v, want expired certificate rejection", err) + } +} + +func TestBrokerCertificateSourceRejectsUnexpectedActor(t *testing.T) { + source, broker := newTestBrokerCertificateSource(t, testAteletIdentity("node-a"), time.Hour) + broker.actorUID = "another-actor-uid" + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if _, err := source.Mint(ctx); err == nil || !strings.Contains(err.Error(), "unexpected actor") { + t.Fatalf("Mint() error = %v, want actor UID rejection", err) + } +} + +type credentialBrokerStub struct { + ateletpb.UnimplementedCredentialBrokerServer + ca *testCA + lifetime time.Duration + publicKeys chan []byte + actorUID string +} + +func (s *credentialBrokerStub) MintActorCertificate(_ context.Context, req *ateletpb.MintActorCertificateRequest) (*ateletpb.MintActorCertificateResponse, error) { + if req.GetExpectedActorUid() != "actor-uid" { + return nil, status.Error(codes.FailedPrecondition, "unexpected actor UID") + } + csr, err := x509.ParseCertificateRequest(req.GetCertificateSigningRequest()) + if err != nil || csr.CheckSignature() != nil { + return nil, status.Error(codes.InvalidArgument, "invalid CSR") + } + now := time.Now() + template := &x509.Certificate{ + SerialNumber: big.NewInt(now.UnixNano()), + Subject: pkix.Name{CommonName: "actor"}, + NotBefore: now.Add(-time.Minute), + NotAfter: now.Add(s.lifetime), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + if err := substratex509.AddActorIdentityToCertificate(&substratex509.ActorIdentity{Atespace: "team", ActorName: "actor", ActorUid: s.actorUID, Purpose: substratex509.ActorIdentityPurposeAtunnel}, template); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + der, err := x509.CreateCertificate(rand.Reader, template, s.ca.cert, csr.PublicKey, s.ca.key) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + s.publicKeys <- csr.RawSubjectPublicKeyInfo + return &ateletpb.MintActorCertificateResponse{ActorCertificates: [][]byte{der}}, nil +} + +func newTestBrokerCertificateSource(t *testing.T, ateletIdentity *substratex509.PodIdentity, lifetime time.Duration) (*BrokerCertificateSource, *credentialBrokerStub) { + t.Helper() + ca := newTestCA(t) + workerCert := issueTestPodCertificate(t, ca, &substratex509.PodIdentity{ + Namespace: "ate-demo", + ServiceAccountName: "ateom", + ServiceAccountUID: "ateom-sa-uid", + PodName: "worker", + PodUID: "worker-uid", + NodeName: "node-a", + NodeUID: "node-uid", + }, "spiffe://cluster.local/ns/ate-demo/sa/ateom", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}) + ateletCert := issueTestPodCertificate(t, ca, ateletIdentity, + "spiffe://cluster.local/ns/ate-system/sa/atelet", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}) + + dir := t.TempDir() + credentialPath := filepath.Join(dir, "worker.pem") + trustPath := filepath.Join(dir, "trust.pem") + writeCredentialBundle(t, credentialPath, workerCert) + if err := os.WriteFile(trustPath, ca.certPEM, 0o600); err != nil { + t.Fatal(err) + } + + clientCAs := x509.NewCertPool() + clientCAs.AppendCertsFromPEM(ca.certPEM) + socketPath := filepath.Join(dir, "credential-broker.sock") + listener, err := net.Listen("unix", socketPath) + if err != nil { + t.Fatal(err) + } + server := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{ + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{ateletCert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCAs, + }))) + broker := &credentialBrokerStub{ca: ca, lifetime: lifetime, publicKeys: make(chan []byte, 2), actorUID: "actor-uid"} + ateletpb.RegisterCredentialBrokerServer(server, broker) + go func() { _ = server.Serve(listener) }() + t.Cleanup(func() { + server.Stop() + _ = listener.Close() + }) + + source, err := NewBrokerCertificateSource(BrokerConfig{ + SocketPath: socketPath, + CredentialBundlePath: credentialPath, + TrustBundlePath: trustPath, + ExpectedActorUID: "actor-uid", + }) + if err != nil { + t.Fatal(err) + } + return source, broker +} + +func testAteletIdentity(nodeName string) *substratex509.PodIdentity { + return &substratex509.PodIdentity{ + Namespace: "ate-system", + ServiceAccountName: "atelet", + ServiceAccountUID: "atelet-sa-uid", + PodName: "atelet", + PodUID: "atelet-uid", + NodeName: nodeName, + NodeUID: "node-uid", + } +} + +func issueTestPodCertificate(t *testing.T, ca *testCA, identity *substratex509.PodIdentity, spiffeID string, usages []x509.ExtKeyUsage) tls.Certificate { + t.Helper() + cert := ca.issue(t, spiffeID, usages) + template, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + t.Fatal(err) + } + if err := substratex509.AddPodIdentityToCertificate(identity, template); err != nil { + t.Fatal(err) + } + key, ok := cert.PrivateKey.(*ecdsa.PrivateKey) + if !ok { + t.Fatalf("private key has type %T", cert.PrivateKey) + } + der, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key) + if err != nil { + t.Fatal(err) + } + cert.Certificate[0] = der + cert.Leaf, err = x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + return cert +} diff --git a/internal/atunnel/egress.go b/internal/atunnel/egress.go index 5b1d63b08..c42899c01 100644 --- a/internal/atunnel/egress.go +++ b/internal/atunnel/egress.go @@ -20,15 +20,22 @@ import ( "fmt" "io" "log/slog" + "math/rand/v2" "net" "sync" + "time" - "github.com/agent-substrate/substrate/internal/resources" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) -// EgressDialer opens an authenticated tunnel to an original destination. -type EgressDialer interface { - DialContext(context.Context, string, EgressMetadata) (net.Conn, error) +// egressDialer opens an authenticated tunnel to an original destination. +type egressDialer interface { + DialContext(context.Context, string) (net.Conn, error) +} + +type actorCertificateSource interface { + Mint(context.Context) (time.Time, error) } // OriginalDestination returns the address that a transparently intercepted @@ -46,11 +53,15 @@ type Egress struct { } type egressActivation struct { - metadata EgressMetadata - dialer EgressDialer - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup + dialer egressDialer + certificateSource actorCertificateSource + expiresAt time.Time + + // ctx scopes certificate renewal and every tunnel opened by this activation. wg + // lets Deactivate wait until both renewal and tunnel forwarding have exited. + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup } // NewEgress creates an activation-aware egress proxy. @@ -88,38 +99,114 @@ func (e *Egress) Serve(ctx context.Context, listener net.Listener) error { } } -// 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 +// renews it until deactivation. +func (e *Egress) Activate(dialer egressDialer, certificateSource actorCertificateSource, expiresAt time.Time) error { if dialer == nil { return fmt.Errorf("atunnel: egress dialer is required") } - if !resources.IsValidResourceName(atespace) || !resources.IsValidResourceName(actorName) { - return fmt.Errorf("atunnel: invalid actor identity %q/%q", atespace, actorName) + if certificateSource == nil { + return fmt.Errorf("atunnel: actor certificate source is required") } - if actorVersion < 1 { - return fmt.Errorf("atunnel: actor version must be positive") + if !expiresAt.After(time.Now()) { + return fmt.Errorf("atunnel: valid actor certificate is required") } e.mu.Lock() defer e.mu.Unlock() if e.active != nil { - return fmt.Errorf("atunnel: actor %s/%s already has active egress", e.active.metadata.Atespace, e.active.metadata.ActorName) - } - ctx, cancel := context.WithCancel(context.Background()) - e.active = &egressActivation{ - metadata: EgressMetadata{ - Atespace: atespace, - ActorName: actorName, - ActorVersion: actorVersion, - BearerToken: bearerToken, - }, - dialer: dialer, - ctx: ctx, - cancel: cancel, + return fmt.Errorf("atunnel: actor already has active egress") + } + activationCtx, cancel := context.WithCancel(context.Background()) + active := &egressActivation{ + dialer: dialer, + certificateSource: certificateSource, + expiresAt: expiresAt, + ctx: activationCtx, + cancel: cancel, } + e.active = active + active.wg.Add(1) + go e.renew(active, expiresAt) return nil } +func (e *Egress) renew(active *egressActivation, expiresAt time.Time) { + defer active.wg.Done() + // Schedule from the credential's remaining lifetime: renew at 90%, then + // keep retrying after expiry so egress can recover without reactivation. + delay := renewAfter(expiresAt) + expired := false + for waitForRenewal(active.ctx, delay) { + if !expiresAt.After(time.Now()) && !expired { + slog.WarnContext(active.ctx, "Atunnel actor certificate expired; blocking new egress connections", + slog.Time("expiredAt", expiresAt)) + expired = true + } + nextExpiry, err := active.certificateSource.Mint(active.ctx) + if err != nil { + code := status.Code(err) + if code == codes.FailedPrecondition || code == codes.PermissionDenied { + e.mu.Lock() + active.expiresAt = time.Time{} + e.mu.Unlock() + slog.WarnContext(active.ctx, "Atunnel actor certificate renewal was denied; blocking new egress connections", + slog.Any("err", err)) + return + } + delay = retryAfter(expiresAt) + continue + } + if !nextExpiry.After(time.Now()) { + delay = retryAfter(expiresAt) + continue + } + e.mu.Lock() + // Check cancellation under the same lock as Deactivate. Whichever wins + // the lock last either installs a live expiry or leaves the activation empty; + // renewal can never restore a credential after deactivation cleared it. + if active.ctx.Err() != nil { + e.mu.Unlock() + return + } + active.expiresAt = nextExpiry + e.mu.Unlock() + if expired { + slog.InfoContext(active.ctx, "Atunnel actor certificate renewed; allowing new egress connections", + slog.Time("expiresAt", nextExpiry)) + expired = false + } + expiresAt = nextExpiry + delay = renewAfter(expiresAt) + } +} + +func renewAfter(expiresAt time.Time) time.Duration { + remaining := time.Until(expiresAt) + return remaining - remaining/10 +} + +func retryAfter(expiresAt time.Time) time.Duration { + remaining := time.Until(expiresAt) + if remaining <= 0 { + return 25*time.Second + rand.N(10*time.Second) + } + return min(30*time.Second, max(time.Second, remaining/10), remaining) +} + +func waitForRenewal(ctx context.Context, delay time.Duration) bool { + if delay <= 0 { + return false + } + timer := time.NewTimer(delay) + defer timer.Stop() + select { + case <-ctx.Done(): + return false + case <-timer.C: + return true + } +} + // Deactivate rejects new egress, closes active streams, and waits for their // forwarding goroutines to exit. func (e *Egress) Deactivate(ctx context.Context) error { @@ -127,6 +214,7 @@ func (e *Egress) Deactivate(ctx context.Context) error { active := e.active e.active = nil if active != nil { + active.expiresAt = time.Time{} active.cancel() } e.mu.Unlock() @@ -155,6 +243,13 @@ func (e *Egress) handle(downstream net.Conn) { _ = downstream.Close() return } + if time.Now().Compare(active.expiresAt) >= 0 { + // Expiry blocks only new tunnels. Connections admitted with a valid + // certificate have completed mTLS and are allowed to drain normally. + e.mu.Unlock() + _ = downstream.Close() + return + } active.wg.Add(1) e.mu.Unlock() @@ -167,7 +262,7 @@ func (e *Egress) handle(downstream net.Conn) { slog.WarnContext(active.ctx, "atunnel failed to resolve original egress destination", slog.Any("err", err)) return } - upstream, err := active.dialer.DialContext(active.ctx, destination, active.metadata) + upstream, err := active.dialer.DialContext(active.ctx, destination) if err != nil { slog.WarnContext(active.ctx, "atunnel failed to open egress tunnel", slog.String("destination", destination), slog.Any("err", err)) return diff --git a/internal/atunnel/egress_test.go b/internal/atunnel/egress_test.go index 17828fbd5..18440ec5f 100644 --- a/internal/atunnel/egress_test.go +++ b/internal/atunnel/egress_test.go @@ -16,30 +16,267 @@ package atunnel import ( "context" + "crypto/tls" + "errors" + "fmt" "io" "net" + "net/http" + "strings" + "sync/atomic" "testing" "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) -func TestEgressForwardsActiveActor(t *testing.T) { - dialed := make(chan egressDial, 1) - upstreamProxy, upstreamGateway := net.Pipe() - t.Cleanup(func() { - _ = upstreamProxy.Close() - _ = upstreamGateway.Close() +func TestEgressActivationFailsClosed(t *testing.T) { + egress, err := NewEgress(func(net.Conn) (string, error) { return "", nil }) + if err != nil { + t.Fatal(err) + } + dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { + t.Fatal("dialed after failed activation") + return nil, nil }) - dialer := egressDialerFunc(func(_ context.Context, destination string, metadata EgressMetadata) (net.Conn, error) { - dialed <- egressDial{destination: destination, metadata: metadata} + if err := egress.Activate(dialer, fakeActorCertificateSource{err: errors.New("renewal failed")}, time.Time{}); err == nil { + t.Fatal("Activate() succeeded") + } + actor, proxy := net.Pipe() + defer actor.Close() + if err := actor.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatal(err) + } + egress.handle(proxy) + if _, err := actor.Read(make([]byte, 1)); err == nil { + t.Fatal("failed activation admitted egress") + } +} + +func TestEgressExpiryRejectsNewButPreservesEstablished(t *testing.T) { + upstreamProxy, upstreamGateway := net.Pipe() + defer upstreamGateway.Close() + var dials atomic.Int32 + var mints atomic.Int32 + dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { + dials.Add(1) return upstreamProxy, nil }) + egress, err := NewEgress(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) + if err != nil { + t.Fatal(err) + } + if err := egress.Activate(dialer, fakeActorCertificateSource{err: errors.New("renewal failed"), calls: &mints}, time.Now().Add(50*time.Millisecond)); err != nil { + t.Fatal(err) + } + actor, proxy := net.Pipe() + defer actor.Close() + egress.handle(proxy) + deadline := time.Now().Add(time.Second) + for dials.Load() == 0 && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + if dials.Load() != 1 { + t.Fatalf("dials = %d, want 1", dials.Load()) + } + time.Sleep(100 * time.Millisecond) + go func() { _, _ = actor.Write([]byte("still-open")) }() + buf := make([]byte, len("still-open")) + if _, err := io.ReadFull(upstreamGateway, buf); err != nil { + t.Fatalf("established tunnel closed after certificate expiry: %v", err) + } + + newActor, newProxy := net.Pipe() + defer newActor.Close() + if err := newActor.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatal(err) + } + egress.handle(newProxy) + if _, err := newActor.Read(make([]byte, 1)); err == nil { + t.Fatal("new tunnel admitted after certificate expiry") + } + if dials.Load() != 1 { + t.Fatalf("dials = %d after expiry, want 1", dials.Load()) + } + if got := mints.Load(); got > 3 { + t.Fatalf("mint attempts = %d, retry loop spun near expiry", got) + } + _ = egress.Deactivate(context.Background()) +} + +func TestEgressRenewsBeforeExpiry(t *testing.T) { + var mints atomic.Int32 + renewed := make(chan struct{}, 1) + renewedExpiry := time.Now().Add(time.Hour) + source := fakeActorCertificateSource{ + expiresAt: renewedExpiry, + calls: &mints, + called: renewed, + } + upstream, gateway := net.Pipe() + defer gateway.Close() + dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { + return upstream, nil + }) + egress, err := NewEgress(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) + if err != nil { + t.Fatal(err) + } + if err := egress.Activate(dialer, source, time.Now().Add(80*time.Millisecond)); err != nil { + t.Fatal(err) + } + select { + case <-renewed: + case <-time.After(time.Second): + t.Fatal("certificate was not renewed") + } + deadline := time.Now().Add(time.Second) + for { + egress.mu.Lock() + expiresAt := egress.active.expiresAt + egress.mu.Unlock() + if expiresAt.Equal(renewedExpiry) { + break + } + if time.Now().After(deadline) { + t.Fatal("renewed certificate expiry was not installed") + } + time.Sleep(time.Millisecond) + } + actor, proxy := net.Pipe() + defer actor.Close() + egress.handle(proxy) + _ = egress.Deactivate(context.Background()) +} + +func TestEgressRetriesRenewalAfterExpiry(t *testing.T) { + for range 100 { + if got := retryAfter(time.Now().Add(-time.Second)); got < 25*time.Second || got >= 35*time.Second { + t.Fatalf("retryAfter(expired) = %v, want [25s, 35s)", got) + } + } +} + +func TestEgressStopsAfterTerminalRenewalFailure(t *testing.T) { + for _, code := range []codes.Code{codes.FailedPrecondition, codes.PermissionDenied} { + t.Run(code.String(), func(t *testing.T) { + called := make(chan struct{}, 1) + egress, err := NewEgress(func(net.Conn) (string, error) { return "", nil }) + if err != nil { + t.Fatal(err) + } + if err := egress.Activate(egressDialerFunc(func(context.Context, string) (net.Conn, error) { + t.Fatal("dialed after renewal was denied") + return nil, nil + }), fakeActorCertificateSource{ + err: fmt.Errorf("mint: %w", status.Error(code, "stale activation")), + called: called, + }, time.Now().Add(50*time.Millisecond)); err != nil { + t.Fatal(err) + } + select { + case <-called: + case <-time.After(time.Second): + t.Fatal("certificate renewal did not start") + } + + deadline := time.Now().Add(time.Second) + for { + egress.mu.Lock() + expiresAt := egress.active.expiresAt + egress.mu.Unlock() + if expiresAt.IsZero() { + break + } + if time.Now().After(deadline) { + t.Fatal("terminal renewal failure did not block new egress") + } + time.Sleep(time.Millisecond) + } + _ = egress.Deactivate(context.Background()) + }) + } +} + +func TestEgressDeactivationDropsConcurrentRenewal(t *testing.T) { + started := make(chan struct{}, 1) + release := make(chan struct{}) + egress, err := NewEgress(func(net.Conn) (string, error) { return "", nil }) + if err != nil { + t.Fatal(err) + } + dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { return nil, nil }) + if err := egress.Activate(dialer, fakeActorCertificateSource{ + expiresAt: time.Now().Add(time.Hour), + called: started, + release: release, + }, time.Now().Add(50*time.Millisecond)); err != nil { + t.Fatal(err) + } + egress.mu.Lock() + active := egress.active + egress.mu.Unlock() + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("certificate renewal did not start") + } + done := make(chan error, 1) + go func() { done <- egress.Deactivate(context.Background()) }() + <-active.ctx.Done() + close(release) + if err := <-done; err != nil { + t.Fatal(err) + } + if !active.expiresAt.IsZero() { + t.Fatalf("deactivated certificate expiry = %v, want zero", active.expiresAt) + } +} + +func TestEgressEndToEnd(t *testing.T) { + ca := newTestCA(t) + requests := make(chan *http.Request, 1) + gatewayDone := make(chan struct{}) + gatewayAddress := serveTestConnectGateway(t, ca, func(conn net.Conn, req *http.Request) { + defer close(gatewayDone) + requests <- req + + tlsConn, ok := conn.(*tls.Conn) + if !ok { + t.Errorf("gateway connection has type %T, want *tls.Conn", conn) + } else { + peer := tlsConn.ConnectionState().PeerCertificates[0] + if len(peer.URIs) != 1 || peer.URIs[0].String() != "spiffe://substrate-actor.local/atespace/team/actor/actor" { + t.Errorf("client identity = %v, want actor SPIFFE ID", peer.URIs) + } + } + + if _, err := io.WriteString(conn, "HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil { + t.Errorf("writing CONNECT response: %v", err) + return + } + payload := make([]byte, len("from actor")) + if _, err := io.ReadFull(conn, payload); err != nil { + t.Errorf("reading actor payload: %v", err) + return + } + if string(payload) != "from actor" { + t.Errorf("gateway payload = %q, want %q", payload, "from actor") + } + if _, err := io.WriteString(conn, "from gateway"); err != nil { + t.Errorf("writing gateway payload: %v", err) + } + }) + client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + egress, err := NewEgress(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) if err != nil { t.Fatal(err) } - if err := egress.Activate(dialer, "team-a", "actor-1", 7, "actor-token"); err != nil { + if err := egress.Activate(client, fakeActorCertificateSource{expiresAt: time.Now().Add(time.Hour)}, time.Now().Add(time.Hour)); err != nil { t.Fatal(err) } @@ -50,33 +287,33 @@ func TestEgressForwardsActiveActor(t *testing.T) { }) egress.handle(downstreamProxy) - gotDial := <-dialed - if gotDial.destination != "192.0.2.10:443" { - t.Errorf("destination = %q, want 192.0.2.10:443", gotDial.destination) + req := <-requests + if req.Method != http.MethodConnect || req.Host != "192.0.2.10:443" { + t.Errorf("request = %s %s, want CONNECT 192.0.2.10:443", req.Method, req.Host) + } + if got := req.Header.Get("Authorization"); got != "" { + t.Errorf("Authorization = %q, want empty", got) } - if gotDial.metadata != (EgressMetadata{Atespace: "team-a", ActorName: "actor-1", ActorVersion: 7, BearerToken: "actor-token"}) { - t.Errorf("metadata = %+v", gotDial.metadata) + for name := range req.Header { + if strings.HasPrefix(strings.ToLower(name), "x-ate-") { + t.Errorf("legacy identity header %q was sent", name) + } } - actorPayload := []byte("from actor") - go func() { _, _ = downstreamActor.Write(actorPayload) }() - gotAtGateway := make([]byte, len(actorPayload)) - if _, err := io.ReadFull(upstreamGateway, gotAtGateway); err != nil { + if err := downstreamActor.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { t.Fatal(err) } - if string(gotAtGateway) != string(actorPayload) { - t.Errorf("gateway payload = %q, want %q", gotAtGateway, actorPayload) + if _, err := io.WriteString(downstreamActor, "from actor"); err != nil { + t.Fatal(err) } - - gatewayPayload := []byte("from gateway") - go func() { _, _ = upstreamGateway.Write(gatewayPayload) }() - gotAtActor := make([]byte, len(gatewayPayload)) + gotAtActor := make([]byte, len("from gateway")) if _, err := io.ReadFull(downstreamActor, gotAtActor); err != nil { t.Fatal(err) } - if string(gotAtActor) != string(gatewayPayload) { - t.Errorf("actor payload = %q, want %q", gotAtActor, gatewayPayload) + if string(gotAtActor) != "from gateway" { + t.Errorf("actor payload = %q, want %q", gotAtActor, "from gateway") } + <-gatewayDone if err := egress.Deactivate(context.Background()); err != nil { t.Fatal(err) @@ -102,13 +339,32 @@ func TestEgressRejectsInactiveConnection(t *testing.T) { } } -type egressDial struct { - destination string - metadata EgressMetadata +type egressDialerFunc func(context.Context, string) (net.Conn, error) + +func (f egressDialerFunc) DialContext(ctx context.Context, destination string) (net.Conn, error) { + return f(ctx, destination) } -type egressDialerFunc func(context.Context, string, EgressMetadata) (net.Conn, error) +type fakeActorCertificateSource struct { + expiresAt time.Time + err error + calls *atomic.Int32 + called chan<- struct{} + release <-chan struct{} +} -func (f egressDialerFunc) DialContext(ctx context.Context, destination string, metadata EgressMetadata) (net.Conn, error) { - return f(ctx, destination, metadata) +func (s fakeActorCertificateSource) Mint(context.Context) (time.Time, error) { + if s.calls != nil { + s.calls.Add(1) + } + if s.called != nil { + select { + case s.called <- struct{}{}: + default: + } + } + if s.release != nil { + <-s.release + } + return s.expiresAt, s.err } diff --git a/internal/atunnel/server.go b/internal/atunnel/ingress.go similarity index 100% rename from internal/atunnel/server.go rename to internal/atunnel/ingress.go diff --git a/internal/atunnel/server_test.go b/internal/atunnel/ingress_test.go similarity index 100% rename from internal/atunnel/server_test.go rename to internal/atunnel/ingress_test.go diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index 5ed205914..f10ee3f80 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -200,6 +200,107 @@ func (SnapshotScope) EnumDescriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{2} } +type MintActorCertificateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // DER-encoded PKCS #10 certificate signing request. Atunnel retains the + // corresponding private key. + CertificateSigningRequest []byte `protobuf:"bytes,1,opt,name=certificate_signing_request,json=certificateSigningRequest,proto3" json:"certificate_signing_request,omitempty"` + // Actor incarnation this activation expects. Ateapi resolves the actor from + // the authenticated worker and rejects the request if its UID differs. + ExpectedActorUid string `protobuf:"bytes,2,opt,name=expected_actor_uid,json=expectedActorUid,proto3" json:"expected_actor_uid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MintActorCertificateRequest) Reset() { + *x = MintActorCertificateRequest{} + mi := &file_atelet_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MintActorCertificateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MintActorCertificateRequest) ProtoMessage() {} + +func (x *MintActorCertificateRequest) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MintActorCertificateRequest.ProtoReflect.Descriptor instead. +func (*MintActorCertificateRequest) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{0} +} + +func (x *MintActorCertificateRequest) GetCertificateSigningRequest() []byte { + if x != nil { + return x.CertificateSigningRequest + } + return nil +} + +func (x *MintActorCertificateRequest) GetExpectedActorUid() string { + if x != nil { + return x.ExpectedActorUid + } + return "" +} + +type MintActorCertificateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // DER-encoded leaf followed by any intermediate certificates. + ActorCertificates [][]byte `protobuf:"bytes,1,rep,name=actor_certificates,json=actorCertificates,proto3" json:"actor_certificates,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MintActorCertificateResponse) Reset() { + *x = MintActorCertificateResponse{} + mi := &file_atelet_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MintActorCertificateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MintActorCertificateResponse) ProtoMessage() {} + +func (x *MintActorCertificateResponse) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MintActorCertificateResponse.ProtoReflect.Descriptor instead. +func (*MintActorCertificateResponse) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{1} +} + +func (x *MintActorCertificateResponse) GetActorCertificates() [][]byte { + if x != nil { + return x.ActorCertificates + } + return nil +} + type RunRequest struct { state protoimpl.MessageState `protogen:"open.v1"` TargetAteomUid string `protobuf:"bytes,1,opt,name=target_ateom_uid,json=targetAteomUid,proto3" json:"target_ateom_uid,omitempty"` @@ -213,13 +314,15 @@ type RunRequest struct { // fetches the relevant assets and records them with the actor's on-node state // so a later Checkpoint can pin the same version into the snapshot manifest. SandboxAssets *SandboxAssets `protobuf:"bytes,8,opt,name=sandbox_assets,json=sandboxAssets,proto3" json:"sandbox_assets,omitempty"` + // When absent, actor traffic uses direct egress instead of atunnel. + EgressGateway *EgressGateway `protobuf:"bytes,9,opt,name=egress_gateway,json=egressGateway,proto3,oneof" json:"egress_gateway,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunRequest) Reset() { *x = RunRequest{} - mi := &file_atelet_proto_msgTypes[0] + mi := &file_atelet_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -231,7 +334,7 @@ func (x *RunRequest) String() string { func (*RunRequest) ProtoMessage() {} func (x *RunRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[0] + mi := &file_atelet_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -244,7 +347,7 @@ func (x *RunRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RunRequest.ProtoReflect.Descriptor instead. func (*RunRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{0} + return file_atelet_proto_rawDescGZIP(), []int{2} } func (x *RunRequest) GetTargetAteomUid() string { @@ -303,6 +406,60 @@ func (x *RunRequest) GetSandboxAssets() *SandboxAssets { return nil } +func (x *RunRequest) GetEgressGateway() *EgressGateway { + if x != nil { + return x.EgressGateway + } + return nil +} + +// EgressGateway configures tunneled egress for one actor activation. +type EgressGateway struct { + state protoimpl.MessageState `protogen:"open.v1"` + // address is required and identifies the remote gateway as an IP address or + // DNS name followed by its port. + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressGateway) Reset() { + *x = EgressGateway{} + mi := &file_atelet_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressGateway) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressGateway) ProtoMessage() {} + +func (x *EgressGateway) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressGateway.ProtoReflect.Descriptor instead. +func (*EgressGateway) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{3} +} + +func (x *EgressGateway) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + // AssetFile is one content-addressed file atelet fetches for a sandbox runtime // (e.g. the gVisor release tarball). type AssetFile struct { @@ -317,7 +474,7 @@ type AssetFile struct { func (x *AssetFile) Reset() { *x = AssetFile{} - mi := &file_atelet_proto_msgTypes[1] + mi := &file_atelet_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -329,7 +486,7 @@ func (x *AssetFile) String() string { func (*AssetFile) ProtoMessage() {} func (x *AssetFile) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[1] + mi := &file_atelet_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -342,7 +499,7 @@ func (x *AssetFile) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetFile.ProtoReflect.Descriptor instead. func (*AssetFile) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{1} + return file_atelet_proto_rawDescGZIP(), []int{4} } func (x *AssetFile) GetUrl() string { @@ -370,7 +527,7 @@ type ArchAssets struct { func (x *ArchAssets) Reset() { *x = ArchAssets{} - mi := &file_atelet_proto_msgTypes[2] + mi := &file_atelet_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -382,7 +539,7 @@ func (x *ArchAssets) String() string { func (*ArchAssets) ProtoMessage() {} func (x *ArchAssets) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[2] + mi := &file_atelet_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -395,7 +552,7 @@ func (x *ArchAssets) ProtoReflect() protoreflect.Message { // Deprecated: Use ArchAssets.ProtoReflect.Descriptor instead. func (*ArchAssets) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{2} + return file_atelet_proto_rawDescGZIP(), []int{5} } func (x *ArchAssets) GetFiles() map[string]*AssetFile { @@ -420,7 +577,7 @@ type SandboxAssets struct { func (x *SandboxAssets) Reset() { *x = SandboxAssets{} - mi := &file_atelet_proto_msgTypes[3] + mi := &file_atelet_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -432,7 +589,7 @@ func (x *SandboxAssets) String() string { func (*SandboxAssets) ProtoMessage() {} func (x *SandboxAssets) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[3] + mi := &file_atelet_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -445,7 +602,7 @@ func (x *SandboxAssets) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxAssets.ProtoReflect.Descriptor instead. func (*SandboxAssets) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{3} + return file_atelet_proto_rawDescGZIP(), []int{6} } func (x *SandboxAssets) GetSandboxClass() string { @@ -474,7 +631,7 @@ type WorkloadSpec struct { func (x *WorkloadSpec) Reset() { *x = WorkloadSpec{} - mi := &file_atelet_proto_msgTypes[4] + mi := &file_atelet_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -486,7 +643,7 @@ func (x *WorkloadSpec) String() string { func (*WorkloadSpec) ProtoMessage() {} func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[4] + mi := &file_atelet_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -499,7 +656,7 @@ func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkloadSpec.ProtoReflect.Descriptor instead. func (*WorkloadSpec) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{4} + return file_atelet_proto_rawDescGZIP(), []int{7} } func (x *WorkloadSpec) GetContainers() []*Container { @@ -531,7 +688,7 @@ type DurableDirVolume struct { func (x *DurableDirVolume) Reset() { *x = DurableDirVolume{} - mi := &file_atelet_proto_msgTypes[5] + mi := &file_atelet_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -543,7 +700,7 @@ func (x *DurableDirVolume) String() string { func (*DurableDirVolume) ProtoMessage() {} func (x *DurableDirVolume) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[5] + mi := &file_atelet_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -556,7 +713,7 @@ func (x *DurableDirVolume) ProtoReflect() protoreflect.Message { // Deprecated: Use DurableDirVolume.ProtoReflect.Descriptor instead. func (*DurableDirVolume) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{5} + return file_atelet_proto_rawDescGZIP(), []int{8} } type ExternalVolumeSource struct { @@ -569,7 +726,7 @@ type ExternalVolumeSource struct { func (x *ExternalVolumeSource) Reset() { *x = ExternalVolumeSource{} - mi := &file_atelet_proto_msgTypes[6] + mi := &file_atelet_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -581,7 +738,7 @@ func (x *ExternalVolumeSource) String() string { func (*ExternalVolumeSource) ProtoMessage() {} func (x *ExternalVolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[6] + mi := &file_atelet_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -594,7 +751,7 @@ func (x *ExternalVolumeSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalVolumeSource.ProtoReflect.Descriptor instead. func (*ExternalVolumeSource) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{6} + return file_atelet_proto_rawDescGZIP(), []int{9} } func (x *ExternalVolumeSource) GetStorageVolumeId() string { @@ -626,7 +783,7 @@ type Volume struct { func (x *Volume) Reset() { *x = Volume{} - mi := &file_atelet_proto_msgTypes[7] + mi := &file_atelet_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -638,7 +795,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[7] + mi := &file_atelet_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -651,7 +808,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{7} + return file_atelet_proto_rawDescGZIP(), []int{10} } func (x *Volume) GetName() string { @@ -719,7 +876,7 @@ type VolumeMount struct { func (x *VolumeMount) Reset() { *x = VolumeMount{} - mi := &file_atelet_proto_msgTypes[8] + mi := &file_atelet_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -731,7 +888,7 @@ func (x *VolumeMount) String() string { func (*VolumeMount) ProtoMessage() {} func (x *VolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[8] + mi := &file_atelet_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -744,7 +901,7 @@ func (x *VolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. func (*VolumeMount) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{8} + return file_atelet_proto_rawDescGZIP(), []int{11} } func (x *VolumeMount) GetName() string { @@ -776,7 +933,7 @@ type Container struct { func (x *Container) Reset() { *x = Container{} - mi := &file_atelet_proto_msgTypes[9] + mi := &file_atelet_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -788,7 +945,7 @@ func (x *Container) String() string { func (*Container) ProtoMessage() {} func (x *Container) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[9] + mi := &file_atelet_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -801,7 +958,7 @@ func (x *Container) ProtoReflect() protoreflect.Message { // Deprecated: Use Container.ProtoReflect.Descriptor instead. func (*Container) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{9} + return file_atelet_proto_rawDescGZIP(), []int{12} } func (x *Container) GetName() string { @@ -863,7 +1020,7 @@ type EnvEntry struct { func (x *EnvEntry) Reset() { *x = EnvEntry{} - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -875,7 +1032,7 @@ func (x *EnvEntry) String() string { func (*EnvEntry) ProtoMessage() {} func (x *EnvEntry) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -888,7 +1045,7 @@ func (x *EnvEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use EnvEntry.ProtoReflect.Descriptor instead. func (*EnvEntry) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{10} + return file_atelet_proto_rawDescGZIP(), []int{13} } func (x *EnvEntry) GetName() string { @@ -919,7 +1076,7 @@ type Readyz struct { func (x *Readyz) Reset() { *x = Readyz{} - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -931,7 +1088,7 @@ func (x *Readyz) String() string { func (*Readyz) ProtoMessage() {} func (x *Readyz) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -944,7 +1101,7 @@ func (x *Readyz) ProtoReflect() protoreflect.Message { // Deprecated: Use Readyz.ProtoReflect.Descriptor instead. func (*Readyz) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{11} + return file_atelet_proto_rawDescGZIP(), []int{14} } func (x *Readyz) GetHttpGet() *HTTPGetAction { @@ -974,7 +1131,7 @@ type HTTPGetAction struct { func (x *HTTPGetAction) Reset() { *x = HTTPGetAction{} - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -986,7 +1143,7 @@ func (x *HTTPGetAction) String() string { func (*HTTPGetAction) ProtoMessage() {} func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -999,7 +1156,7 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. func (*HTTPGetAction) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{12} + return file_atelet_proto_rawDescGZIP(), []int{15} } func (x *HTTPGetAction) GetPath() string { @@ -1024,7 +1181,7 @@ type RunResponse struct { func (x *RunResponse) Reset() { *x = RunResponse{} - mi := &file_atelet_proto_msgTypes[13] + mi := &file_atelet_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1036,7 +1193,7 @@ func (x *RunResponse) String() string { func (*RunResponse) ProtoMessage() {} func (x *RunResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[13] + mi := &file_atelet_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1049,7 +1206,7 @@ func (x *RunResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunResponse.ProtoReflect.Descriptor instead. func (*RunResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{13} + return file_atelet_proto_rawDescGZIP(), []int{16} } type LocalCheckpointConfiguration struct { @@ -1063,7 +1220,7 @@ type LocalCheckpointConfiguration struct { func (x *LocalCheckpointConfiguration) Reset() { *x = LocalCheckpointConfiguration{} - mi := &file_atelet_proto_msgTypes[14] + mi := &file_atelet_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1075,7 +1232,7 @@ func (x *LocalCheckpointConfiguration) String() string { func (*LocalCheckpointConfiguration) ProtoMessage() {} func (x *LocalCheckpointConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[14] + mi := &file_atelet_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1088,7 +1245,7 @@ func (x *LocalCheckpointConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use LocalCheckpointConfiguration.ProtoReflect.Descriptor instead. func (*LocalCheckpointConfiguration) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{14} + return file_atelet_proto_rawDescGZIP(), []int{17} } func (x *LocalCheckpointConfiguration) GetSnapshotPrefix() string { @@ -1117,7 +1274,7 @@ type ExternalCheckpointConfiguration struct { func (x *ExternalCheckpointConfiguration) Reset() { *x = ExternalCheckpointConfiguration{} - mi := &file_atelet_proto_msgTypes[15] + mi := &file_atelet_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1129,7 +1286,7 @@ func (x *ExternalCheckpointConfiguration) String() string { func (*ExternalCheckpointConfiguration) ProtoMessage() {} func (x *ExternalCheckpointConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[15] + mi := &file_atelet_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1142,7 +1299,7 @@ func (x *ExternalCheckpointConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalCheckpointConfiguration.ProtoReflect.Descriptor instead. func (*ExternalCheckpointConfiguration) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{15} + return file_atelet_proto_rawDescGZIP(), []int{18} } func (x *ExternalCheckpointConfiguration) GetSnapshotUriPrefix() string { @@ -1180,7 +1337,7 @@ type CheckpointRequest struct { func (x *CheckpointRequest) Reset() { *x = CheckpointRequest{} - mi := &file_atelet_proto_msgTypes[16] + mi := &file_atelet_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1192,7 +1349,7 @@ func (x *CheckpointRequest) String() string { func (*CheckpointRequest) ProtoMessage() {} func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[16] + mi := &file_atelet_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1205,7 +1362,7 @@ func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointRequest.ProtoReflect.Descriptor instead. func (*CheckpointRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{16} + return file_atelet_proto_rawDescGZIP(), []int{19} } func (x *CheckpointRequest) GetTargetAteomUid() string { @@ -1320,7 +1477,7 @@ type CheckpointResponse struct { func (x *CheckpointResponse) Reset() { *x = CheckpointResponse{} - mi := &file_atelet_proto_msgTypes[17] + mi := &file_atelet_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1332,7 +1489,7 @@ func (x *CheckpointResponse) String() string { func (*CheckpointResponse) ProtoMessage() {} func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[17] + mi := &file_atelet_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1345,7 +1502,7 @@ func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointResponse.ProtoReflect.Descriptor instead. func (*CheckpointResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{17} + return file_atelet_proto_rawDescGZIP(), []int{20} } type RestoreRequest struct { @@ -1377,13 +1534,15 @@ type RestoreRequest struct { // of the `config` oneof: the actor's snapshot may be local (a pause // checkpoint) while the golden snapshot is always external. GoldenSnapshotUriPrefix string `protobuf:"bytes,12,opt,name=golden_snapshot_uri_prefix,json=goldenSnapshotUriPrefix,proto3" json:"golden_snapshot_uri_prefix,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // When absent, actor traffic uses direct egress instead of atunnel. + EgressGateway *EgressGateway `protobuf:"bytes,13,opt,name=egress_gateway,json=egressGateway,proto3,oneof" json:"egress_gateway,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RestoreRequest) Reset() { *x = RestoreRequest{} - mi := &file_atelet_proto_msgTypes[18] + mi := &file_atelet_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1395,7 +1554,7 @@ func (x *RestoreRequest) String() string { func (*RestoreRequest) ProtoMessage() {} func (x *RestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[18] + mi := &file_atelet_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1408,7 +1567,7 @@ func (x *RestoreRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreRequest.ProtoReflect.Descriptor instead. func (*RestoreRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{18} + return file_atelet_proto_rawDescGZIP(), []int{21} } func (x *RestoreRequest) GetTargetAteomUid() string { @@ -1506,6 +1665,13 @@ func (x *RestoreRequest) GetGoldenSnapshotUriPrefix() string { return "" } +func (x *RestoreRequest) GetEgressGateway() *EgressGateway { + if x != nil { + return x.EgressGateway + } + return nil +} + type isRestoreRequest_Config interface { isRestoreRequest_Config() } @@ -1530,7 +1696,7 @@ type RestoreResponse struct { func (x *RestoreResponse) Reset() { *x = RestoreResponse{} - mi := &file_atelet_proto_msgTypes[19] + mi := &file_atelet_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1542,7 +1708,7 @@ func (x *RestoreResponse) String() string { func (*RestoreResponse) ProtoMessage() {} func (x *RestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[19] + mi := &file_atelet_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1555,14 +1721,19 @@ func (x *RestoreResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreResponse.ProtoReflect.Descriptor instead. func (*RestoreResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{19} + return file_atelet_proto_rawDescGZIP(), []int{22} } var File_atelet_proto protoreflect.FileDescriptor const file_atelet_proto_rawDesc = "" + "\n" + - "\fatelet.proto\x12\x06atelet\"\xe0\x02\n" + + "\fatelet.proto\x12\x06atelet\"\x8b\x01\n" + + "\x1bMintActorCertificateRequest\x12>\n" + + "\x1bcertificate_signing_request\x18\x01 \x01(\fR\x19certificateSigningRequest\x12,\n" + + "\x12expected_actor_uid\x18\x02 \x01(\tR\x10expectedActorUid\"M\n" + + "\x1cMintActorCertificateResponse\x12-\n" + + "\x12actor_certificates\x18\x01 \x03(\fR\x11actorCertificates\"\xb6\x03\n" + "\n" + "RunRequest\x12(\n" + "\x10target_ateom_uid\x18\x01 \x01(\tR\x0etargetAteomUid\x12\x1a\n" + @@ -1573,7 +1744,11 @@ const file_atelet_proto_rawDesc = "" + "\x18actor_template_namespace\x18\x05 \x01(\tR\x16actorTemplateNamespace\x12.\n" + "\x13actor_template_name\x18\x06 \x01(\tR\x11actorTemplateName\x12(\n" + "\x04spec\x18\a \x01(\v2\x14.atelet.WorkloadSpecR\x04spec\x12<\n" + - "\x0esandbox_assets\x18\b \x01(\v2\x15.atelet.SandboxAssetsR\rsandboxAssets\"5\n" + + "\x0esandbox_assets\x18\b \x01(\v2\x15.atelet.SandboxAssetsR\rsandboxAssets\x12A\n" + + "\x0eegress_gateway\x18\t \x01(\v2\x15.atelet.EgressGatewayH\x00R\regressGateway\x88\x01\x01B\x11\n" + + "\x0f_egress_gateway\")\n" + + "\rEgressGateway\x12\x18\n" + + "\aaddress\x18\x01 \x01(\tR\aaddress\"5\n" + "\tAssetFile\x12\x10\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n" + "\x06sha256\x18\x02 \x01(\tR\x06sha256\"\x8e\x01\n" + @@ -1650,7 +1825,7 @@ const file_atelet_proto_rawDesc = "" + " \x01(\v2'.atelet.ExternalCheckpointConfigurationH\x00R\x0eexternalConfig\x12+\n" + "\x05scope\x18\v \x01(\x0e2\x15.atelet.SnapshotScopeR\x05scopeB\b\n" + "\x06config\"\x14\n" + - "\x12CheckpointResponse\"\xe5\x04\n" + + "\x12CheckpointResponse\"\xbb\x05\n" + "\x0eRestoreRequest\x12(\n" + "\x10target_ateom_uid\x18\x01 \x01(\tR\x0etargetAteomUid\x12\x1a\n" + "\batespace\x18\x02 \x01(\tR\batespace\x12\x1d\n" + @@ -1665,8 +1840,10 @@ const file_atelet_proto_rawDesc = "" + "\x0fexternal_config\x18\n" + " \x01(\v2'.atelet.ExternalCheckpointConfigurationH\x00R\x0eexternalConfig\x12+\n" + "\x05scope\x18\v \x01(\x0e2\x15.atelet.SnapshotScopeR\x05scope\x12;\n" + - "\x1agolden_snapshot_uri_prefix\x18\f \x01(\tR\x17goldenSnapshotUriPrefixB\b\n" + - "\x06config\"\x11\n" + + "\x1agolden_snapshot_uri_prefix\x18\f \x01(\tR\x17goldenSnapshotUriPrefix\x12A\n" + + "\x0eegress_gateway\x18\r \x01(\v2\x15.atelet.EgressGatewayH\x01R\regressGateway\x88\x01\x01B\b\n" + + "\x06configB\x11\n" + + "\x0f_egress_gateway\"\x11\n" + "\x0fRestoreResponse*`\n" + "\n" + "VolumeType\x12\x1b\n" + @@ -1681,7 +1858,9 @@ const file_atelet_proto_rawDesc = "" + "\x1aSNAPSHOT_SCOPE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13SNAPSHOT_SCOPE_FULL\x10\x01\x12\x17\n" + "\x13SNAPSHOT_SCOPE_DATA\x10\x02\x12!\n" + - "\x1dSNAPSHOT_SCOPE_DATA_ON_GOLDEN\x10\x032\xc4\x01\n" + + "\x1dSNAPSHOT_SCOPE_DATA_ON_GOLDEN\x10\x032w\n" + + "\x10CredentialBroker\x12c\n" + + "\x14MintActorCertificate\x12#.atelet.MintActorCertificateRequest\x1a$.atelet.MintActorCertificateResponse\"\x002\xc4\x01\n" + "\vAteomHerder\x120\n" + "\x03Run\x12\x12.atelet.RunRequest\x1a\x13.atelet.RunResponse\"\x00\x12E\n" + "\n" + @@ -1701,71 +1880,78 @@ func file_atelet_proto_rawDescGZIP() []byte { } var file_atelet_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_atelet_proto_goTypes = []any{ (VolumeType)(0), // 0: atelet.VolumeType (CheckpointType)(0), // 1: atelet.CheckpointType (SnapshotScope)(0), // 2: atelet.SnapshotScope - (*RunRequest)(nil), // 3: atelet.RunRequest - (*AssetFile)(nil), // 4: atelet.AssetFile - (*ArchAssets)(nil), // 5: atelet.ArchAssets - (*SandboxAssets)(nil), // 6: atelet.SandboxAssets - (*WorkloadSpec)(nil), // 7: atelet.WorkloadSpec - (*DurableDirVolume)(nil), // 8: atelet.DurableDirVolume - (*ExternalVolumeSource)(nil), // 9: atelet.ExternalVolumeSource - (*Volume)(nil), // 10: atelet.Volume - (*VolumeMount)(nil), // 11: atelet.VolumeMount - (*Container)(nil), // 12: atelet.Container - (*EnvEntry)(nil), // 13: atelet.EnvEntry - (*Readyz)(nil), // 14: atelet.Readyz - (*HTTPGetAction)(nil), // 15: atelet.HTTPGetAction - (*RunResponse)(nil), // 16: atelet.RunResponse - (*LocalCheckpointConfiguration)(nil), // 17: atelet.LocalCheckpointConfiguration - (*ExternalCheckpointConfiguration)(nil), // 18: atelet.ExternalCheckpointConfiguration - (*CheckpointRequest)(nil), // 19: atelet.CheckpointRequest - (*CheckpointResponse)(nil), // 20: atelet.CheckpointResponse - (*RestoreRequest)(nil), // 21: atelet.RestoreRequest - (*RestoreResponse)(nil), // 22: atelet.RestoreResponse - nil, // 23: atelet.ArchAssets.FilesEntry - nil, // 24: atelet.SandboxAssets.AssetsEntry + (*MintActorCertificateRequest)(nil), // 3: atelet.MintActorCertificateRequest + (*MintActorCertificateResponse)(nil), // 4: atelet.MintActorCertificateResponse + (*RunRequest)(nil), // 5: atelet.RunRequest + (*EgressGateway)(nil), // 6: atelet.EgressGateway + (*AssetFile)(nil), // 7: atelet.AssetFile + (*ArchAssets)(nil), // 8: atelet.ArchAssets + (*SandboxAssets)(nil), // 9: atelet.SandboxAssets + (*WorkloadSpec)(nil), // 10: atelet.WorkloadSpec + (*DurableDirVolume)(nil), // 11: atelet.DurableDirVolume + (*ExternalVolumeSource)(nil), // 12: atelet.ExternalVolumeSource + (*Volume)(nil), // 13: atelet.Volume + (*VolumeMount)(nil), // 14: atelet.VolumeMount + (*Container)(nil), // 15: atelet.Container + (*EnvEntry)(nil), // 16: atelet.EnvEntry + (*Readyz)(nil), // 17: atelet.Readyz + (*HTTPGetAction)(nil), // 18: atelet.HTTPGetAction + (*RunResponse)(nil), // 19: atelet.RunResponse + (*LocalCheckpointConfiguration)(nil), // 20: atelet.LocalCheckpointConfiguration + (*ExternalCheckpointConfiguration)(nil), // 21: atelet.ExternalCheckpointConfiguration + (*CheckpointRequest)(nil), // 22: atelet.CheckpointRequest + (*CheckpointResponse)(nil), // 23: atelet.CheckpointResponse + (*RestoreRequest)(nil), // 24: atelet.RestoreRequest + (*RestoreResponse)(nil), // 25: atelet.RestoreResponse + nil, // 26: atelet.ArchAssets.FilesEntry + nil, // 27: atelet.SandboxAssets.AssetsEntry } var file_atelet_proto_depIdxs = []int32{ - 7, // 0: atelet.RunRequest.spec:type_name -> atelet.WorkloadSpec - 6, // 1: atelet.RunRequest.sandbox_assets:type_name -> atelet.SandboxAssets - 23, // 2: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry - 24, // 3: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry - 12, // 4: atelet.WorkloadSpec.containers:type_name -> atelet.Container - 10, // 5: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume - 0, // 6: atelet.Volume.type:type_name -> atelet.VolumeType - 8, // 7: atelet.Volume.durable_dir:type_name -> atelet.DurableDirVolume - 9, // 8: atelet.Volume.external:type_name -> atelet.ExternalVolumeSource - 13, // 9: atelet.Container.env:type_name -> atelet.EnvEntry - 14, // 10: atelet.Container.readyz:type_name -> atelet.Readyz - 11, // 11: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount - 15, // 12: atelet.Readyz.http_get:type_name -> atelet.HTTPGetAction - 7, // 13: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec - 1, // 14: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType - 17, // 15: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration - 18, // 16: atelet.CheckpointRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration - 2, // 17: atelet.CheckpointRequest.scope:type_name -> atelet.SnapshotScope - 7, // 18: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec - 1, // 19: atelet.RestoreRequest.type:type_name -> atelet.CheckpointType - 17, // 20: atelet.RestoreRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration - 18, // 21: atelet.RestoreRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration - 2, // 22: atelet.RestoreRequest.scope:type_name -> atelet.SnapshotScope - 4, // 23: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile - 5, // 24: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets - 3, // 25: atelet.AteomHerder.Run:input_type -> atelet.RunRequest - 19, // 26: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest - 21, // 27: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest - 16, // 28: atelet.AteomHerder.Run:output_type -> atelet.RunResponse - 20, // 29: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse - 22, // 30: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse - 28, // [28:31] is the sub-list for method output_type - 25, // [25:28] is the sub-list for method input_type - 25, // [25:25] is the sub-list for extension type_name - 25, // [25:25] is the sub-list for extension extendee - 0, // [0:25] is the sub-list for field type_name + 10, // 0: atelet.RunRequest.spec:type_name -> atelet.WorkloadSpec + 9, // 1: atelet.RunRequest.sandbox_assets:type_name -> atelet.SandboxAssets + 6, // 2: atelet.RunRequest.egress_gateway:type_name -> atelet.EgressGateway + 26, // 3: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry + 27, // 4: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry + 15, // 5: atelet.WorkloadSpec.containers:type_name -> atelet.Container + 13, // 6: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume + 0, // 7: atelet.Volume.type:type_name -> atelet.VolumeType + 11, // 8: atelet.Volume.durable_dir:type_name -> atelet.DurableDirVolume + 12, // 9: atelet.Volume.external:type_name -> atelet.ExternalVolumeSource + 16, // 10: atelet.Container.env:type_name -> atelet.EnvEntry + 17, // 11: atelet.Container.readyz:type_name -> atelet.Readyz + 14, // 12: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount + 18, // 13: atelet.Readyz.http_get:type_name -> atelet.HTTPGetAction + 10, // 14: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec + 1, // 15: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType + 20, // 16: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration + 21, // 17: atelet.CheckpointRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration + 2, // 18: atelet.CheckpointRequest.scope:type_name -> atelet.SnapshotScope + 10, // 19: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec + 1, // 20: atelet.RestoreRequest.type:type_name -> atelet.CheckpointType + 20, // 21: atelet.RestoreRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration + 21, // 22: atelet.RestoreRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration + 2, // 23: atelet.RestoreRequest.scope:type_name -> atelet.SnapshotScope + 6, // 24: atelet.RestoreRequest.egress_gateway:type_name -> atelet.EgressGateway + 7, // 25: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile + 8, // 26: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets + 3, // 27: atelet.CredentialBroker.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest + 5, // 28: atelet.AteomHerder.Run:input_type -> atelet.RunRequest + 22, // 29: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest + 24, // 30: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest + 4, // 31: atelet.CredentialBroker.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse + 19, // 32: atelet.AteomHerder.Run:output_type -> atelet.RunResponse + 23, // 33: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse + 25, // 34: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse + 31, // [31:35] is the sub-list for method output_type + 27, // [27:31] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_atelet_proto_init() } @@ -1773,15 +1959,16 @@ func file_atelet_proto_init() { if File_atelet_proto != nil { return } - file_atelet_proto_msgTypes[7].OneofWrappers = []any{ + file_atelet_proto_msgTypes[2].OneofWrappers = []any{} + file_atelet_proto_msgTypes[10].OneofWrappers = []any{ (*Volume_DurableDir)(nil), (*Volume_External)(nil), } - file_atelet_proto_msgTypes[16].OneofWrappers = []any{ + file_atelet_proto_msgTypes[19].OneofWrappers = []any{ (*CheckpointRequest_LocalConfig)(nil), (*CheckpointRequest_ExternalConfig)(nil), } - file_atelet_proto_msgTypes[18].OneofWrappers = []any{ + file_atelet_proto_msgTypes[21].OneofWrappers = []any{ (*RestoreRequest_LocalConfig)(nil), (*RestoreRequest_ExternalConfig)(nil), } @@ -1791,9 +1978,9 @@ func file_atelet_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_atelet_proto_rawDesc), len(file_atelet_proto_rawDesc)), NumEnums: 3, - NumMessages: 22, + NumMessages: 25, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_atelet_proto_goTypes, DependencyIndexes: file_atelet_proto_depIdxs, diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 1aab6cc3b..1a85421cd 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -18,6 +18,26 @@ package atelet; option go_package = "github.com/agent-substrate/substrate/internal/proto/ateletpb"; +// CredentialBroker gives an authenticated worker its current actor credential. +service CredentialBroker { + rpc MintActorCertificate(MintActorCertificateRequest) returns (MintActorCertificateResponse) {} +} + +message MintActorCertificateRequest { + // DER-encoded PKCS #10 certificate signing request. Atunnel retains the + // corresponding private key. + bytes certificate_signing_request = 1; + + // Actor incarnation this activation expects. Ateapi resolves the actor from + // the authenticated worker and rejects the request if its UID differs. + string expected_actor_uid = 2; +} + +message MintActorCertificateResponse { + // DER-encoded leaf followed by any intermediate certificates. + repeated bytes actor_certificates = 1; +} + service AteomHerder { // Run tells atelet to create a new containerized workload from scratch on an // ateom. @@ -48,6 +68,16 @@ message RunRequest { // fetches the relevant assets and records them with the actor's on-node state // so a later Checkpoint can pin the same version into the snapshot manifest. SandboxAssets sandbox_assets = 8; + + // When absent, actor traffic uses direct egress instead of atunnel. + optional EgressGateway egress_gateway = 9; +} + +// EgressGateway configures tunneled egress for one actor activation. +message EgressGateway { + // address is required and identifies the remote gateway as an IP address or + // DNS name followed by its port. + string address = 1; } // AssetFile is one content-addressed file atelet fetches for a sandbox runtime @@ -258,6 +288,9 @@ message RestoreRequest { // of the `config` oneof: the actor's snapshot may be local (a pause // checkpoint) while the golden snapshot is always external. string golden_snapshot_uri_prefix = 12; + + // When absent, actor traffic uses direct egress instead of atunnel. + optional EgressGateway egress_gateway = 13; } message RestoreResponse { diff --git a/internal/proto/ateletpb/atelet_grpc.pb.go b/internal/proto/ateletpb/atelet_grpc.pb.go index 4f05a878d..e4e6d1923 100644 --- a/internal/proto/ateletpb/atelet_grpc.pb.go +++ b/internal/proto/ateletpb/atelet_grpc.pb.go @@ -32,6 +32,112 @@ import ( // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 +const ( + CredentialBroker_MintActorCertificate_FullMethodName = "/atelet.CredentialBroker/MintActorCertificate" +) + +// CredentialBrokerClient is the client API for CredentialBroker service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// CredentialBroker gives an authenticated worker its current actor credential. +type CredentialBrokerClient interface { + MintActorCertificate(ctx context.Context, in *MintActorCertificateRequest, opts ...grpc.CallOption) (*MintActorCertificateResponse, error) +} + +type credentialBrokerClient struct { + cc grpc.ClientConnInterface +} + +func NewCredentialBrokerClient(cc grpc.ClientConnInterface) CredentialBrokerClient { + return &credentialBrokerClient{cc} +} + +func (c *credentialBrokerClient) MintActorCertificate(ctx context.Context, in *MintActorCertificateRequest, opts ...grpc.CallOption) (*MintActorCertificateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MintActorCertificateResponse) + err := c.cc.Invoke(ctx, CredentialBroker_MintActorCertificate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CredentialBrokerServer is the server API for CredentialBroker service. +// All implementations must embed UnimplementedCredentialBrokerServer +// for forward compatibility. +// +// CredentialBroker gives an authenticated worker its current actor credential. +type CredentialBrokerServer interface { + MintActorCertificate(context.Context, *MintActorCertificateRequest) (*MintActorCertificateResponse, error) + mustEmbedUnimplementedCredentialBrokerServer() +} + +// UnimplementedCredentialBrokerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedCredentialBrokerServer struct{} + +func (UnimplementedCredentialBrokerServer) MintActorCertificate(context.Context, *MintActorCertificateRequest) (*MintActorCertificateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method MintActorCertificate not implemented") +} +func (UnimplementedCredentialBrokerServer) mustEmbedUnimplementedCredentialBrokerServer() {} +func (UnimplementedCredentialBrokerServer) testEmbeddedByValue() {} + +// UnsafeCredentialBrokerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CredentialBrokerServer will +// result in compilation errors. +type UnsafeCredentialBrokerServer interface { + mustEmbedUnimplementedCredentialBrokerServer() +} + +func RegisterCredentialBrokerServer(s grpc.ServiceRegistrar, srv CredentialBrokerServer) { + // If the following call panics, it indicates UnimplementedCredentialBrokerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&CredentialBroker_ServiceDesc, srv) +} + +func _CredentialBroker_MintActorCertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MintActorCertificateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CredentialBrokerServer).MintActorCertificate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CredentialBroker_MintActorCertificate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CredentialBrokerServer).MintActorCertificate(ctx, req.(*MintActorCertificateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// CredentialBroker_ServiceDesc is the grpc.ServiceDesc for CredentialBroker service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CredentialBroker_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "atelet.CredentialBroker", + HandlerType: (*CredentialBrokerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "MintActorCertificate", + Handler: _CredentialBroker_MintActorCertificate_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "atelet.proto", +} + const ( AteomHerder_Run_FullMethodName = "/atelet.AteomHerder/Run" AteomHerder_Checkpoint_FullMethodName = "/atelet.AteomHerder/Checkpoint" diff --git a/internal/proto/ateompb/ateom.pb.go b/internal/proto/ateompb/ateom.pb.go index 7cd226315..bba84a09b 100644 --- a/internal/proto/ateompb/ateom.pb.go +++ b/internal/proto/ateompb/ateom.pb.go @@ -99,26 +99,23 @@ func (SnapshotScope) EnumDescriptor() ([]byte, []int) { } type RunWorkloadRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - // Actor resource version observed by ate-api when assigning this worker. - ActorVersion int64 `protobuf:"varint,9,opt,name=actor_version,json=actorVersion,proto3" json:"actor_version,omitempty"` - ActorTemplateNamespace string `protobuf:"bytes,4,opt,name=actor_template_namespace,json=actorTemplateNamespace,proto3" json:"actor_template_namespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` - RunscPath string `protobuf:"bytes,6,opt,name=runsc_path,json=runscPath,proto3" json:"runsc_path,omitempty"` - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` + ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + ActorTemplateNamespace string `protobuf:"bytes,4,opt,name=actor_template_namespace,json=actorTemplateNamespace,proto3" json:"actor_template_namespace,omitempty"` + ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + RunscPath string `protobuf:"bytes,6,opt,name=runsc_path,json=runscPath,proto3" json:"runsc_path,omitempty"` + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` // runtime_asset_paths maps a runtime asset name (e.g. "cloud-hypervisor", // "virtiofsd", "kata-kernel", "kata-image", "kata-config") // to the local on-disk path atelet fetched it to (content-addressed, like // runsc_path). Empty for the gVisor runtime, which uses runsc_path. RuntimeAssetPaths map[string]string `protobuf:"bytes,8,rep,name=runtime_asset_paths,json=runtimeAssetPaths,proto3" json:"runtime_asset_paths,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Remote egress gateway selected for this activation. When absent, actor - // traffic uses direct egress instead of being redirected through atunnel. - EgressGatewayAddress *string `protobuf:"bytes,10,opt,name=egress_gateway_address,json=egressGatewayAddress,proto3,oneof" json:"egress_gateway_address,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // When absent, actor traffic uses direct egress instead of atunnel. + EgressGateway *EgressGateway `protobuf:"bytes,10,opt,name=egress_gateway,json=egressGateway,proto3,oneof" json:"egress_gateway,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RunWorkloadRequest) Reset() { @@ -172,13 +169,6 @@ func (x *RunWorkloadRequest) GetActorUid() string { return "" } -func (x *RunWorkloadRequest) GetActorVersion() int64 { - if x != nil { - return x.ActorVersion - } - return 0 -} - func (x *RunWorkloadRequest) GetActorTemplateNamespace() string { if x != nil { return x.ActorTemplateNamespace @@ -214,9 +204,56 @@ func (x *RunWorkloadRequest) GetRuntimeAssetPaths() map[string]string { return nil } -func (x *RunWorkloadRequest) GetEgressGatewayAddress() string { - if x != nil && x.EgressGatewayAddress != nil { - return *x.EgressGatewayAddress +func (x *RunWorkloadRequest) GetEgressGateway() *EgressGateway { + if x != nil { + return x.EgressGateway + } + return nil +} + +// EgressGateway configures tunneled egress for one actor activation. +type EgressGateway struct { + state protoimpl.MessageState `protogen:"open.v1"` + // address is required and identifies the remote gateway as an IP address or + // DNS name followed by its port. + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressGateway) Reset() { + *x = EgressGateway{} + mi := &file_ateom_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressGateway) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressGateway) ProtoMessage() {} + +func (x *EgressGateway) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressGateway.ProtoReflect.Descriptor instead. +func (*EgressGateway) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{1} +} + +func (x *EgressGateway) GetAddress() string { + if x != nil { + return x.Address } return "" } @@ -231,7 +268,7 @@ type WorkloadSpec struct { func (x *WorkloadSpec) Reset() { *x = WorkloadSpec{} - mi := &file_ateom_proto_msgTypes[1] + mi := &file_ateom_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -243,7 +280,7 @@ func (x *WorkloadSpec) String() string { func (*WorkloadSpec) ProtoMessage() {} func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[1] + mi := &file_ateom_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -256,7 +293,7 @@ func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkloadSpec.ProtoReflect.Descriptor instead. func (*WorkloadSpec) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{1} + return file_ateom_proto_rawDescGZIP(), []int{2} } func (x *WorkloadSpec) GetContainers() []*Container { @@ -279,7 +316,7 @@ type Container struct { func (x *Container) Reset() { *x = Container{} - mi := &file_ateom_proto_msgTypes[2] + mi := &file_ateom_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -291,7 +328,7 @@ func (x *Container) String() string { func (*Container) ProtoMessage() {} func (x *Container) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[2] + mi := &file_ateom_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -304,7 +341,7 @@ func (x *Container) ProtoReflect() protoreflect.Message { // Deprecated: Use Container.ProtoReflect.Descriptor instead. func (*Container) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{2} + return file_ateom_proto_rawDescGZIP(), []int{3} } func (x *Container) GetName() string { @@ -342,7 +379,7 @@ type DurableDirVolumeMount struct { func (x *DurableDirVolumeMount) Reset() { *x = DurableDirVolumeMount{} - mi := &file_ateom_proto_msgTypes[3] + mi := &file_ateom_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -354,7 +391,7 @@ func (x *DurableDirVolumeMount) String() string { func (*DurableDirVolumeMount) ProtoMessage() {} func (x *DurableDirVolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[3] + mi := &file_ateom_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -367,7 +404,7 @@ func (x *DurableDirVolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use DurableDirVolumeMount.ProtoReflect.Descriptor instead. func (*DurableDirVolumeMount) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{3} + return file_ateom_proto_rawDescGZIP(), []int{4} } func (x *DurableDirVolumeMount) GetVolumeName() string { @@ -398,7 +435,7 @@ type Readyz struct { func (x *Readyz) Reset() { *x = Readyz{} - mi := &file_ateom_proto_msgTypes[4] + mi := &file_ateom_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -410,7 +447,7 @@ func (x *Readyz) String() string { func (*Readyz) ProtoMessage() {} func (x *Readyz) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[4] + mi := &file_ateom_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -423,7 +460,7 @@ func (x *Readyz) ProtoReflect() protoreflect.Message { // Deprecated: Use Readyz.ProtoReflect.Descriptor instead. func (*Readyz) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{4} + return file_ateom_proto_rawDescGZIP(), []int{5} } func (x *Readyz) GetHttpGet() *HTTPGetAction { @@ -453,7 +490,7 @@ type HTTPGetAction struct { func (x *HTTPGetAction) Reset() { *x = HTTPGetAction{} - mi := &file_ateom_proto_msgTypes[5] + mi := &file_ateom_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -465,7 +502,7 @@ func (x *HTTPGetAction) String() string { func (*HTTPGetAction) ProtoMessage() {} func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[5] + mi := &file_ateom_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -478,7 +515,7 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. func (*HTTPGetAction) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{5} + return file_ateom_proto_rawDescGZIP(), []int{6} } func (x *HTTPGetAction) GetPath() string { @@ -503,7 +540,7 @@ type RunWorkloadResponse struct { func (x *RunWorkloadResponse) Reset() { *x = RunWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[6] + mi := &file_ateom_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -515,7 +552,7 @@ func (x *RunWorkloadResponse) String() string { func (*RunWorkloadResponse) ProtoMessage() {} func (x *RunWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[6] + mi := &file_ateom_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -528,7 +565,7 @@ func (x *RunWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunWorkloadResponse.ProtoReflect.Descriptor instead. func (*RunWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{6} + return file_ateom_proto_rawDescGZIP(), []int{7} } type CheckpointWorkloadRequest struct { @@ -560,7 +597,7 @@ type CheckpointWorkloadRequest struct { func (x *CheckpointWorkloadRequest) Reset() { *x = CheckpointWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[7] + mi := &file_ateom_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -572,7 +609,7 @@ func (x *CheckpointWorkloadRequest) String() string { func (*CheckpointWorkloadRequest) ProtoMessage() {} func (x *CheckpointWorkloadRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[7] + mi := &file_ateom_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -585,7 +622,7 @@ func (x *CheckpointWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadRequest.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{7} + return file_ateom_proto_rawDescGZIP(), []int{8} } func (x *CheckpointWorkloadRequest) GetAtespace() string { @@ -670,7 +707,7 @@ type CheckpointWorkloadResponse struct { func (x *CheckpointWorkloadResponse) Reset() { *x = CheckpointWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[8] + mi := &file_ateom_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -682,7 +719,7 @@ func (x *CheckpointWorkloadResponse) String() string { func (*CheckpointWorkloadResponse) ProtoMessage() {} func (x *CheckpointWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[8] + mi := &file_ateom_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -695,7 +732,7 @@ func (x *CheckpointWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadResponse.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{8} + return file_ateom_proto_rawDescGZIP(), []int{9} } func (x *CheckpointWorkloadResponse) GetSnapshotFiles() []string { @@ -706,16 +743,14 @@ func (x *CheckpointWorkloadResponse) GetSnapshotFiles() []string { } type RestoreWorkloadRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - // Actor resource version observed by ate-api when assigning this worker. - ActorVersion int64 `protobuf:"varint,11,opt,name=actor_version,json=actorVersion,proto3" json:"actor_version,omitempty"` - ActorTemplateNamespace string `protobuf:"bytes,4,opt,name=actor_template_namespace,json=actorTemplateNamespace,proto3" json:"actor_template_namespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` - RunscPath string `protobuf:"bytes,6,opt,name=runsc_path,json=runscPath,proto3" json:"runsc_path,omitempty"` - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` + ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + ActorTemplateNamespace string `protobuf:"bytes,4,opt,name=actor_template_namespace,json=actorTemplateNamespace,proto3" json:"actor_template_namespace,omitempty"` + ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + RunscPath string `protobuf:"bytes,6,opt,name=runsc_path,json=runscPath,proto3" json:"runsc_path,omitempty"` + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` // The object storage URI prefix of the snapshot to restore. SnapshotUriPrefix string `protobuf:"bytes,8,opt,name=snapshot_uri_prefix,json=snapshotUriPrefix,proto3" json:"snapshot_uri_prefix,omitempty"` // runtime_asset_paths maps a runtime asset name to the local on-disk path @@ -723,9 +758,8 @@ type RestoreWorkloadRequest struct { RuntimeAssetPaths map[string]string `protobuf:"bytes,9,rep,name=runtime_asset_paths,json=runtimeAssetPaths,proto3" json:"runtime_asset_paths,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // What content to restore from the snapshot. Scope SnapshotScope `protobuf:"varint,10,opt,name=scope,proto3,enum=ateom.SnapshotScope" json:"scope,omitempty"` - // Remote egress gateway selected for this activation. When absent, actor - // traffic uses direct egress instead of being redirected through atunnel. - EgressGatewayAddress *string `protobuf:"bytes,12,opt,name=egress_gateway_address,json=egressGatewayAddress,proto3,oneof" json:"egress_gateway_address,omitempty"` + // When absent, actor traffic uses direct egress instead of atunnel. + EgressGateway *EgressGateway `protobuf:"bytes,12,opt,name=egress_gateway,json=egressGateway,proto3,oneof" json:"egress_gateway,omitempty"` // The object storage URI prefix of the ActorTemplate's golden snapshot. // Set only when scope is SNAPSHOT_SCOPE_DATA_ON_GOLDEN. Mirrors the // snapshot_uri_prefix contract (field 8). @@ -736,7 +770,7 @@ type RestoreWorkloadRequest struct { func (x *RestoreWorkloadRequest) Reset() { *x = RestoreWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[9] + mi := &file_ateom_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -748,7 +782,7 @@ func (x *RestoreWorkloadRequest) String() string { func (*RestoreWorkloadRequest) ProtoMessage() {} func (x *RestoreWorkloadRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[9] + mi := &file_ateom_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -761,7 +795,7 @@ func (x *RestoreWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadRequest.ProtoReflect.Descriptor instead. func (*RestoreWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{9} + return file_ateom_proto_rawDescGZIP(), []int{10} } func (x *RestoreWorkloadRequest) GetAtespace() string { @@ -785,13 +819,6 @@ func (x *RestoreWorkloadRequest) GetActorUid() string { return "" } -func (x *RestoreWorkloadRequest) GetActorVersion() int64 { - if x != nil { - return x.ActorVersion - } - return 0 -} - func (x *RestoreWorkloadRequest) GetActorTemplateNamespace() string { if x != nil { return x.ActorTemplateNamespace @@ -841,11 +868,11 @@ func (x *RestoreWorkloadRequest) GetScope() SnapshotScope { return SnapshotScope_SNAPSHOT_SCOPE_UNSPECIFIED } -func (x *RestoreWorkloadRequest) GetEgressGatewayAddress() string { - if x != nil && x.EgressGatewayAddress != nil { - return *x.EgressGatewayAddress +func (x *RestoreWorkloadRequest) GetEgressGateway() *EgressGateway { + if x != nil { + return x.EgressGateway } - return "" + return nil } func (x *RestoreWorkloadRequest) GetGoldenSnapshotUriPrefix() string { @@ -863,7 +890,7 @@ type RestoreWorkloadResponse struct { func (x *RestoreWorkloadResponse) Reset() { *x = RestoreWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[10] + mi := &file_ateom_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -875,7 +902,7 @@ func (x *RestoreWorkloadResponse) String() string { func (*RestoreWorkloadResponse) ProtoMessage() {} func (x *RestoreWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[10] + mi := &file_ateom_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -888,32 +915,33 @@ func (x *RestoreWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadResponse.ProtoReflect.Descriptor instead. func (*RestoreWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{10} + return file_ateom_proto_rawDescGZIP(), []int{11} } var File_ateom_proto protoreflect.FileDescriptor const file_ateom_proto_rawDesc = "" + "\n" + - "\vateom.proto\x12\x05ateom\"\xc1\x04\n" + + "\vateom.proto\x12\x05ateom\"\x9b\x04\n" + "\x12RunWorkloadRequest\x12\x1a\n" + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + "\n" + "actor_name\x18\x02 \x01(\tR\tactorName\x12\x1b\n" + - "\tactor_uid\x18\x03 \x01(\tR\bactorUid\x12#\n" + - "\ractor_version\x18\t \x01(\x03R\factorVersion\x128\n" + + "\tactor_uid\x18\x03 \x01(\tR\bactorUid\x128\n" + "\x18actor_template_namespace\x18\x04 \x01(\tR\x16actorTemplateNamespace\x12.\n" + "\x13actor_template_name\x18\x05 \x01(\tR\x11actorTemplateName\x12\x1d\n" + "\n" + "runsc_path\x18\x06 \x01(\tR\trunscPath\x12'\n" + "\x04spec\x18\a \x01(\v2\x13.ateom.WorkloadSpecR\x04spec\x12`\n" + - "\x13runtime_asset_paths\x18\b \x03(\v20.ateom.RunWorkloadRequest.RuntimeAssetPathsEntryR\x11runtimeAssetPaths\x129\n" + - "\x16egress_gateway_address\x18\n" + - " \x01(\tH\x00R\x14egressGatewayAddress\x88\x01\x01\x1aD\n" + + "\x13runtime_asset_paths\x18\b \x03(\v20.ateom.RunWorkloadRequest.RuntimeAssetPathsEntryR\x11runtimeAssetPaths\x12@\n" + + "\x0eegress_gateway\x18\n" + + " \x01(\v2\x14.ateom.EgressGatewayH\x00R\regressGateway\x88\x01\x01\x1aD\n" + "\x16RuntimeAssetPathsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x19\n" + - "\x17_egress_gateway_address\"@\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x11\n" + + "\x0f_egress_gateway\")\n" + + "\rEgressGateway\x12\x18\n" + + "\aaddress\x18\x01 \x01(\tR\aaddress\"@\n" + "\fWorkloadSpec\x120\n" + "\n" + "containers\x18\x01 \x03(\v2\x10.ateom.ContainerR\n" + @@ -952,13 +980,12 @@ const file_ateom_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"C\n" + "\x1aCheckpointWorkloadResponse\x12%\n" + - "\x0esnapshot_files\x18\x01 \x03(\tR\rsnapshotFiles\"\xe2\x05\n" + + "\x0esnapshot_files\x18\x01 \x03(\tR\rsnapshotFiles\"\xbc\x05\n" + "\x16RestoreWorkloadRequest\x12\x1a\n" + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + "\n" + "actor_name\x18\x02 \x01(\tR\tactorName\x12\x1b\n" + - "\tactor_uid\x18\x03 \x01(\tR\bactorUid\x12#\n" + - "\ractor_version\x18\v \x01(\x03R\factorVersion\x128\n" + + "\tactor_uid\x18\x03 \x01(\tR\bactorUid\x128\n" + "\x18actor_template_namespace\x18\x04 \x01(\tR\x16actorTemplateNamespace\x12.\n" + "\x13actor_template_name\x18\x05 \x01(\tR\x11actorTemplateName\x12\x1d\n" + "\n" + @@ -967,13 +994,13 @@ const file_ateom_proto_rawDesc = "" + "\x13snapshot_uri_prefix\x18\b \x01(\tR\x11snapshotUriPrefix\x12d\n" + "\x13runtime_asset_paths\x18\t \x03(\v24.ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntryR\x11runtimeAssetPaths\x12*\n" + "\x05scope\x18\n" + - " \x01(\x0e2\x14.ateom.SnapshotScopeR\x05scope\x129\n" + - "\x16egress_gateway_address\x18\f \x01(\tH\x00R\x14egressGatewayAddress\x88\x01\x01\x12;\n" + + " \x01(\x0e2\x14.ateom.SnapshotScopeR\x05scope\x12@\n" + + "\x0eegress_gateway\x18\f \x01(\v2\x14.ateom.EgressGatewayH\x00R\regressGateway\x88\x01\x01\x12;\n" + "\x1agolden_snapshot_uri_prefix\x18\r \x01(\tR\x17goldenSnapshotUriPrefix\x1aD\n" + "\x16RuntimeAssetPathsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x19\n" + - "\x17_egress_gateway_address\"\x19\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x11\n" + + "\x0f_egress_gateway\"\x19\n" + "\x17RestoreWorkloadResponse*\x84\x01\n" + "\rSnapshotScope\x12\x1e\n" + "\x1aSNAPSHOT_SCOPE_UNSPECIFIED\x10\x00\x12\x17\n" + @@ -998,48 +1025,51 @@ func file_ateom_proto_rawDescGZIP() []byte { } var file_ateom_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_ateom_proto_goTypes = []any{ (SnapshotScope)(0), // 0: ateom.SnapshotScope (*RunWorkloadRequest)(nil), // 1: ateom.RunWorkloadRequest - (*WorkloadSpec)(nil), // 2: ateom.WorkloadSpec - (*Container)(nil), // 3: ateom.Container - (*DurableDirVolumeMount)(nil), // 4: ateom.DurableDirVolumeMount - (*Readyz)(nil), // 5: ateom.Readyz - (*HTTPGetAction)(nil), // 6: ateom.HTTPGetAction - (*RunWorkloadResponse)(nil), // 7: ateom.RunWorkloadResponse - (*CheckpointWorkloadRequest)(nil), // 8: ateom.CheckpointWorkloadRequest - (*CheckpointWorkloadResponse)(nil), // 9: ateom.CheckpointWorkloadResponse - (*RestoreWorkloadRequest)(nil), // 10: ateom.RestoreWorkloadRequest - (*RestoreWorkloadResponse)(nil), // 11: ateom.RestoreWorkloadResponse - nil, // 12: ateom.RunWorkloadRequest.RuntimeAssetPathsEntry - nil, // 13: ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry - nil, // 14: ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry + (*EgressGateway)(nil), // 2: ateom.EgressGateway + (*WorkloadSpec)(nil), // 3: ateom.WorkloadSpec + (*Container)(nil), // 4: ateom.Container + (*DurableDirVolumeMount)(nil), // 5: ateom.DurableDirVolumeMount + (*Readyz)(nil), // 6: ateom.Readyz + (*HTTPGetAction)(nil), // 7: ateom.HTTPGetAction + (*RunWorkloadResponse)(nil), // 8: ateom.RunWorkloadResponse + (*CheckpointWorkloadRequest)(nil), // 9: ateom.CheckpointWorkloadRequest + (*CheckpointWorkloadResponse)(nil), // 10: ateom.CheckpointWorkloadResponse + (*RestoreWorkloadRequest)(nil), // 11: ateom.RestoreWorkloadRequest + (*RestoreWorkloadResponse)(nil), // 12: ateom.RestoreWorkloadResponse + nil, // 13: ateom.RunWorkloadRequest.RuntimeAssetPathsEntry + nil, // 14: ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry + nil, // 15: ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry } var file_ateom_proto_depIdxs = []int32{ - 2, // 0: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 12, // 1: ateom.RunWorkloadRequest.runtime_asset_paths:type_name -> ateom.RunWorkloadRequest.RuntimeAssetPathsEntry - 3, // 2: ateom.WorkloadSpec.containers:type_name -> ateom.Container - 5, // 3: ateom.Container.readyz:type_name -> ateom.Readyz - 4, // 4: ateom.Container.durable_dir_volume_mounts:type_name -> ateom.DurableDirVolumeMount - 6, // 5: ateom.Readyz.http_get:type_name -> ateom.HTTPGetAction - 2, // 6: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 13, // 7: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry - 0, // 8: ateom.CheckpointWorkloadRequest.scope:type_name -> ateom.SnapshotScope - 2, // 9: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 14, // 10: ateom.RestoreWorkloadRequest.runtime_asset_paths:type_name -> ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry - 0, // 11: ateom.RestoreWorkloadRequest.scope:type_name -> ateom.SnapshotScope - 1, // 12: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest - 8, // 13: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest - 10, // 14: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest - 7, // 15: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse - 9, // 16: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse - 11, // 17: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse - 15, // [15:18] is the sub-list for method output_type - 12, // [12:15] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 3, // 0: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 13, // 1: ateom.RunWorkloadRequest.runtime_asset_paths:type_name -> ateom.RunWorkloadRequest.RuntimeAssetPathsEntry + 2, // 2: ateom.RunWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway + 4, // 3: ateom.WorkloadSpec.containers:type_name -> ateom.Container + 6, // 4: ateom.Container.readyz:type_name -> ateom.Readyz + 5, // 5: ateom.Container.durable_dir_volume_mounts:type_name -> ateom.DurableDirVolumeMount + 7, // 6: ateom.Readyz.http_get:type_name -> ateom.HTTPGetAction + 3, // 7: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 14, // 8: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry + 0, // 9: ateom.CheckpointWorkloadRequest.scope:type_name -> ateom.SnapshotScope + 3, // 10: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 15, // 11: ateom.RestoreWorkloadRequest.runtime_asset_paths:type_name -> ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry + 0, // 12: ateom.RestoreWorkloadRequest.scope:type_name -> ateom.SnapshotScope + 2, // 13: ateom.RestoreWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway + 1, // 14: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest + 9, // 15: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest + 11, // 16: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest + 8, // 17: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse + 10, // 18: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse + 12, // 19: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse + 17, // [17:20] is the sub-list for method output_type + 14, // [14:17] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_ateom_proto_init() } @@ -1048,14 +1078,14 @@ func file_ateom_proto_init() { return } file_ateom_proto_msgTypes[0].OneofWrappers = []any{} - file_ateom_proto_msgTypes[9].OneofWrappers = []any{} + file_ateom_proto_msgTypes[10].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ateom_proto_rawDesc), len(file_ateom_proto_rawDesc)), NumEnums: 1, - NumMessages: 14, + NumMessages: 15, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/ateompb/ateom.proto b/internal/proto/ateompb/ateom.proto index c5f2293bd..f539beaf5 100644 --- a/internal/proto/ateompb/ateom.proto +++ b/internal/proto/ateompb/ateom.proto @@ -51,9 +51,6 @@ message RunWorkloadRequest { string atespace = 1; string actor_name = 2; string actor_uid = 3; - // Actor resource version observed by ate-api when assigning this worker. - int64 actor_version = 9; - string actor_template_namespace = 4; string actor_template_name = 5; @@ -67,9 +64,15 @@ message RunWorkloadRequest { // runsc_path). Empty for the gVisor runtime, which uses runsc_path. map runtime_asset_paths = 8; - // Remote egress gateway selected for this activation. When absent, actor - // traffic uses direct egress instead of being redirected through atunnel. - optional string egress_gateway_address = 10; + // When absent, actor traffic uses direct egress instead of atunnel. + optional EgressGateway egress_gateway = 10; +} + +// EgressGateway configures tunneled egress for one actor activation. +message EgressGateway { + // address is required and identifies the remote gateway as an IP address or + // DNS name followed by its port. + string address = 1; } // WorkloadSpec parallels Pod, but with far fewer configurable fields. @@ -179,9 +182,6 @@ message RestoreWorkloadRequest { string atespace = 1; string actor_name = 2; string actor_uid = 3; - // Actor resource version observed by ate-api when assigning this worker. - int64 actor_version = 11; - string actor_template_namespace = 4; string actor_template_name = 5; @@ -199,9 +199,8 @@ message RestoreWorkloadRequest { // What content to restore from the snapshot. SnapshotScope scope = 10; - // Remote egress gateway selected for this activation. When absent, actor - // traffic uses direct egress instead of being redirected through atunnel. - optional string egress_gateway_address = 12; + // When absent, actor traffic uses direct egress instead of atunnel. + optional EgressGateway egress_gateway = 12; // The object storage URI prefix of the ActorTemplate's golden snapshot. // Set only when scope is SNAPSHOT_SCOPE_DATA_ON_GOLDEN. Mirrors the diff --git a/internal/substratex509/substratex509.go b/internal/substratex509/substratex509.go index 474f00177..e6445b902 100644 --- a/internal/substratex509/substratex509.go +++ b/internal/substratex509/substratex509.go @@ -136,10 +136,15 @@ func validatePodIdentity(pod *PodIdentity) error { // ActorIdentity is the Substrate Actor Identity of an Actor, as embedded in the // oidActorIdentity extension of its certificate. +type ActorIdentityPurpose string + +const ActorIdentityPurposeAtunnel ActorIdentityPurpose = "atunnel" + type ActorIdentity struct { Atespace string ActorName string ActorUid string + Purpose ActorIdentityPurpose } func AddActorIdentityToCertificate(actor *ActorIdentity, template *x509.Certificate) error { @@ -200,8 +205,14 @@ func validateActorIdentity(actor *ActorIdentity) error { if actor.ActorUid == "" { empty = append(empty, "ActorUid") } + if actor.Purpose == "" { + empty = append(empty, "Purpose") + } if len(empty) > 0 { return fmt.Errorf("empty fields: %s", strings.Join(empty, ", ")) } + if actor.Purpose != ActorIdentityPurposeAtunnel { + return fmt.Errorf("unsupported Purpose %q", actor.Purpose) + } return nil } diff --git a/internal/substratex509/substratex509_test.go b/internal/substratex509/substratex509_test.go index 4e5a9172f..156855fe6 100644 --- a/internal/substratex509/substratex509_test.go +++ b/internal/substratex509/substratex509_test.go @@ -224,6 +224,7 @@ func TestActorIdentityFromCertificate(t *testing.T) { Atespace: "team-a", ActorName: "researcher", ActorUid: "actor-uid", + Purpose: ActorIdentityPurposeAtunnel, } for _, tc := range []struct { name string @@ -288,6 +289,7 @@ func TestActorIdentityFromCertificate(t *testing.T) { actor := ActorIdentity{ Atespace: "team-a", ActorName: "researcher", + Purpose: ActorIdentityPurposeAtunnel, } value, err := json.Marshal(actor) if err != nil { @@ -335,6 +337,7 @@ func TestAddActorIdentityToCertificateEmptyField(t *testing.T) { {"Atespace", func(a *ActorIdentity) { a.Atespace = "" }}, {"ActorName", func(a *ActorIdentity) { a.ActorName = "" }}, {"ActorUid", func(a *ActorIdentity) { a.ActorUid = "" }}, + {"Purpose", func(a *ActorIdentity) { a.Purpose = "" }}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -343,6 +346,7 @@ func TestAddActorIdentityToCertificateEmptyField(t *testing.T) { Atespace: "team-a", ActorName: "researcher", ActorUid: "actor-uid", + Purpose: ActorIdentityPurposeAtunnel, } tc.mutate(&actor) err := AddActorIdentityToCertificate(&actor, &x509.Certificate{}) @@ -372,6 +376,7 @@ func TestExtensionValueIsJSON(t *testing.T) { Atespace: "team-a", ActorName: "researcher", ActorUid: "actor-uid", + Purpose: ActorIdentityPurposeAtunnel, } template := &x509.Certificate{} if err := AddPodIdentityToCertificate(&pod, template); err != nil { diff --git a/manifests/ate-install/atelet.yaml b/manifests/ate-install/atelet.yaml index 9d830d71c..7e6c08397 100644 --- a/manifests/ate-install/atelet.yaml +++ b/manifests/ate-install/atelet.yaml @@ -77,6 +77,7 @@ spec: - --gcp-auth-for-image-pulls=true - --grpc-server-cred-bundle=/run/podidentity.podcert.ate.dev/credential-bundle.pem - --client-ca-certs=/run/podidentity.podcert.ate.dev/trust-bundle.pem + - --ateapi-ca-file=/run/servicedns.podcert.ate.dev/trust-bundle.pem # Graceful shutdown knobs. The sum must fit within # terminationGracePeriodSeconds above. - --drain-delay=0s @@ -97,10 +98,6 @@ spec: drop: - ALL env: - - name: MY_NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - name: POD_NAME valueFrom: fieldRef: @@ -150,6 +147,9 @@ spec: mountPath: /var/lib/ateom-gvisor - name: podidentity mountPath: /run/podidentity.podcert.ate.dev + - name: servicedns-ca + mountPath: /run/servicedns.podcert.ate.dev + readOnly: true volumes: - name: run-ateom hostPath: @@ -172,3 +172,12 @@ spec: matchLabels: podcert.ate.dev/canarying: live path: trust-bundle.pem + - name: servicedns-ca + projected: + sources: + - clusterTrustBundle: + signerName: servicedns.podcert.ate.dev/identity + labelSelector: + matchLabels: + podcert.ate.dev/canarying: live + path: trust-bundle.pem diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index fdae6906f..6a8e68892 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -138,6 +138,52 @@ func (ActorSnapshotTagScope) EnumDescriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{1} } +type ActorCertificatePurpose int32 + +const ( + ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED ActorCertificatePurpose = 0 + ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_ATUNNEL ActorCertificatePurpose = 1 +) + +// Enum value maps for ActorCertificatePurpose. +var ( + ActorCertificatePurpose_name = map[int32]string{ + 0: "ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED", + 1: "ACTOR_CERTIFICATE_PURPOSE_ATUNNEL", + } + ActorCertificatePurpose_value = map[string]int32{ + "ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED": 0, + "ACTOR_CERTIFICATE_PURPOSE_ATUNNEL": 1, + } +) + +func (x ActorCertificatePurpose) Enum() *ActorCertificatePurpose { + p := new(ActorCertificatePurpose) + *p = x + return p +} + +func (x ActorCertificatePurpose) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ActorCertificatePurpose) Descriptor() protoreflect.EnumDescriptor { + return file_ateapi_proto_enumTypes[2].Descriptor() +} + +func (ActorCertificatePurpose) Type() protoreflect.EnumType { + return &file_ateapi_proto_enumTypes[2] +} + +func (x ActorCertificatePurpose) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ActorCertificatePurpose.Descriptor instead. +func (ActorCertificatePurpose) EnumDescriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{2} +} + type ExternalVolume_Status int32 const ( @@ -177,11 +223,11 @@ func (x ExternalVolume_Status) String() string { } func (ExternalVolume_Status) Descriptor() protoreflect.EnumDescriptor { - return file_ateapi_proto_enumTypes[2].Descriptor() + return file_ateapi_proto_enumTypes[3].Descriptor() } func (ExternalVolume_Status) Type() protoreflect.EnumType { - return &file_ateapi_proto_enumTypes[2] + return &file_ateapi_proto_enumTypes[3] } func (x ExternalVolume_Status) Number() protoreflect.EnumNumber { @@ -244,11 +290,11 @@ func (x Actor_Status) String() string { } func (Actor_Status) Descriptor() protoreflect.EnumDescriptor { - return file_ateapi_proto_enumTypes[3].Descriptor() + return file_ateapi_proto_enumTypes[4].Descriptor() } func (Actor_Status) Type() protoreflect.EnumType { - return &file_ateapi_proto_enumTypes[3] + return &file_ateapi_proto_enumTypes[4] } func (x Actor_Status) Number() protoreflect.EnumNumber { @@ -293,11 +339,11 @@ func (x Worker_State) String() string { } func (Worker_State) Descriptor() protoreflect.EnumDescriptor { - return file_ateapi_proto_enumTypes[4].Descriptor() + return file_ateapi_proto_enumTypes[5].Descriptor() } func (Worker_State) Type() protoreflect.EnumType { - return &file_ateapi_proto_enumTypes[4] + return &file_ateapi_proto_enumTypes[5] } func (x Worker_State) Number() protoreflect.EnumNumber { @@ -2849,16 +2895,23 @@ func (x *MintJWTResponse) GetActorJwt() string { } type MintCertRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // Worker identity authenticated by the node-local atelet. Ateapi resolves + // the worker's current actor assignment rather than trusting actor metadata + // from the caller. + WorkerNamespace string `protobuf:"bytes,1,opt,name=worker_namespace,json=workerNamespace,proto3" json:"worker_namespace,omitempty"` + WorkerPod string `protobuf:"bytes,2,opt,name=worker_pod,json=workerPod,proto3" json:"worker_pod,omitempty"` + WorkerPodUid string `protobuf:"bytes,3,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` // Request contains DER encoded bytes of a x509 certificate signing request. // The signer will ignore the contents of the CSR except to extract the // subject public key. CertificateSigningRequest []byte `protobuf:"bytes,4,opt,name=certificate_signing_request,json=certificateSigningRequest,proto3" json:"certificate_signing_request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Actor incarnation expected by the activation. This is only a stale-request + // guard: ateapi derives the actor and its identity from the worker assignment. + ExpectedActorUid string `protobuf:"bytes,5,opt,name=expected_actor_uid,json=expectedActorUid,proto3" json:"expected_actor_uid,omitempty"` + Purpose ActorCertificatePurpose `protobuf:"varint,6,opt,name=purpose,proto3,enum=ateapi.ActorCertificatePurpose" json:"purpose,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MintCertRequest) Reset() { @@ -2891,23 +2944,23 @@ func (*MintCertRequest) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{43} } -func (x *MintCertRequest) GetAtespace() string { +func (x *MintCertRequest) GetWorkerNamespace() string { if x != nil { - return x.Atespace + return x.WorkerNamespace } return "" } -func (x *MintCertRequest) GetActorName() string { +func (x *MintCertRequest) GetWorkerPod() string { if x != nil { - return x.ActorName + return x.WorkerPod } return "" } -func (x *MintCertRequest) GetActorUid() string { +func (x *MintCertRequest) GetWorkerPodUid() string { if x != nil { - return x.ActorUid + return x.WorkerPodUid } return "" } @@ -2919,6 +2972,20 @@ func (x *MintCertRequest) GetCertificateSigningRequest() []byte { return nil } +func (x *MintCertRequest) GetExpectedActorUid() string { + if x != nil { + return x.ExpectedActorUid + } + return "" +} + +func (x *MintCertRequest) GetPurpose() ActorCertificatePurpose { + if x != nil { + return x.Purpose + } + return ActorCertificatePurpose_ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED +} + type MintCertResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Response contains a list of DER encoded certificates. The first entry is the @@ -3164,13 +3231,15 @@ const file_ateapi_proto_rawDesc = "" + "actor_name\x18\x03 \x01(\tR\tactorName\x12\x1b\n" + "\tactor_uid\x18\x04 \x01(\tR\bactorUid\".\n" + "\x0fMintJWTResponse\x12\x1b\n" + - "\tactor_jwt\x18\x01 \x01(\tR\bactorJwt\"\xa9\x01\n" + - "\x0fMintCertRequest\x12\x1a\n" + - "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + + "\tactor_jwt\x18\x01 \x01(\tR\bactorJwt\"\xaa\x02\n" + + "\x0fMintCertRequest\x12)\n" + + "\x10worker_namespace\x18\x01 \x01(\tR\x0fworkerNamespace\x12\x1d\n" + "\n" + - "actor_name\x18\x02 \x01(\tR\tactorName\x12\x1b\n" + - "\tactor_uid\x18\x03 \x01(\tR\bactorUid\x12>\n" + - "\x1bcertificate_signing_request\x18\x04 \x01(\fR\x19certificateSigningRequest\"A\n" + + "worker_pod\x18\x02 \x01(\tR\tworkerPod\x12$\n" + + "\x0eworker_pod_uid\x18\x03 \x01(\tR\fworkerPodUid\x12>\n" + + "\x1bcertificate_signing_request\x18\x04 \x01(\fR\x19certificateSigningRequest\x12,\n" + + "\x12expected_actor_uid\x18\x05 \x01(\tR\x10expectedActorUid\x129\n" + + "\apurpose\x18\x06 \x01(\x0e2\x1f.ateapi.ActorCertificatePurposeR\apurpose\"A\n" + "\x10MintCertResponse\x12-\n" + "\x12actor_certificates\x18\x01 \x03(\fR\x11actorCertificates*\x80\x01\n" + "\x14SnapshotContentScope\x12&\n" + @@ -3179,7 +3248,10 @@ const file_ateapi_proto_rawDesc = "" + "\x1bSNAPSHOT_CONTENT_SCOPE_DATA\x10\x02*f\n" + "\x15ActorSnapshotTagScope\x12%\n" + "!ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE\x10\x00\x12&\n" + - "\"ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED\x10\x012\xb3\n" + + "\"ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED\x10\x01*k\n" + + "\x17ActorCertificatePurpose\x12)\n" + + "%ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED\x10\x00\x12%\n" + + "!ACTOR_CERTIFICATE_PURPOSE_ATUNNEL\x10\x012\xb3\n" + "\n" + "\aControl\x124\n" + "\bGetActor\x12\x17.ateapi.GetActorRequest\x1a\r.ateapi.Actor\"\x00\x12:\n" + @@ -3221,162 +3293,164 @@ func file_ateapi_proto_rawDescGZIP() []byte { return file_ateapi_proto_rawDescData } -var file_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 6) var file_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 47) var file_ateapi_proto_goTypes = []any{ (SnapshotContentScope)(0), // 0: ateapi.SnapshotContentScope (ActorSnapshotTagScope)(0), // 1: ateapi.ActorSnapshotTagScope - (ExternalVolume_Status)(0), // 2: ateapi.ExternalVolume.Status - (Actor_Status)(0), // 3: ateapi.Actor.Status - (Worker_State)(0), // 4: ateapi.Worker.State - (*LocalSnapshotInfo)(nil), // 5: ateapi.LocalSnapshotInfo - (*Selector)(nil), // 6: ateapi.Selector - (*ResourceMetadata)(nil), // 7: ateapi.ResourceMetadata - (*ExternalVolume)(nil), // 8: ateapi.ExternalVolume - (*Actor)(nil), // 9: ateapi.Actor - (*WorkerAssignment)(nil), // 10: ateapi.WorkerAssignment - (*ActorSnapshot)(nil), // 11: ateapi.ActorSnapshot - (*ActorSnapshotTag)(nil), // 12: ateapi.ActorSnapshotTag - (*Atespace)(nil), // 13: ateapi.Atespace - (*ObjectRef)(nil), // 14: ateapi.ObjectRef - (*ActorSnapshotRef)(nil), // 15: ateapi.ActorSnapshotRef - (*CreateAtespaceRequest)(nil), // 16: ateapi.CreateAtespaceRequest - (*GetAtespaceRequest)(nil), // 17: ateapi.GetAtespaceRequest - (*ListAtespacesRequest)(nil), // 18: ateapi.ListAtespacesRequest - (*ListAtespacesResponse)(nil), // 19: ateapi.ListAtespacesResponse - (*DeleteAtespaceRequest)(nil), // 20: ateapi.DeleteAtespaceRequest - (*GetActorRequest)(nil), // 21: ateapi.GetActorRequest - (*CreateActorRequest)(nil), // 22: ateapi.CreateActorRequest - (*UpdateActorRequest)(nil), // 23: ateapi.UpdateActorRequest - (*SuspendActorRequest)(nil), // 24: ateapi.SuspendActorRequest - (*SuspendActorResponse)(nil), // 25: ateapi.SuspendActorResponse - (*PauseActorRequest)(nil), // 26: ateapi.PauseActorRequest - (*PauseActorResponse)(nil), // 27: ateapi.PauseActorResponse - (*ResumeActorRequest)(nil), // 28: ateapi.ResumeActorRequest - (*ResumeActorResponse)(nil), // 29: ateapi.ResumeActorResponse - (*DeleteActorRequest)(nil), // 30: ateapi.DeleteActorRequest - (*GetActorSnapshotRequest)(nil), // 31: ateapi.GetActorSnapshotRequest - (*ListActorSnapshotsRequest)(nil), // 32: ateapi.ListActorSnapshotsRequest - (*ListActorSnapshotsResponse)(nil), // 33: ateapi.ListActorSnapshotsResponse - (*TagActorSnapshotRequest)(nil), // 34: ateapi.TagActorSnapshotRequest - (*UpdateActorSnapshotTagRequest)(nil), // 35: ateapi.UpdateActorSnapshotTagRequest - (*DeleteActorSnapshotTagRequest)(nil), // 36: ateapi.DeleteActorSnapshotTagRequest - (*ListWorkersRequest)(nil), // 37: ateapi.ListWorkersRequest - (*ListWorkersResponse)(nil), // 38: ateapi.ListWorkersResponse - (*ListActorsRequest)(nil), // 39: ateapi.ListActorsRequest - (*ListActorsResponse)(nil), // 40: ateapi.ListActorsResponse - (*Worker)(nil), // 41: ateapi.Worker - (*Assignment)(nil), // 42: ateapi.Assignment - (*KubeNamespacedObjectRef)(nil), // 43: ateapi.KubeNamespacedObjectRef - (*DebugClearRequest)(nil), // 44: ateapi.DebugClearRequest - (*DebugClearResponse)(nil), // 45: ateapi.DebugClearResponse - (*MintJWTRequest)(nil), // 46: ateapi.MintJWTRequest - (*MintJWTResponse)(nil), // 47: ateapi.MintJWTResponse - (*MintCertRequest)(nil), // 48: ateapi.MintCertRequest - (*MintCertResponse)(nil), // 49: ateapi.MintCertResponse - nil, // 50: ateapi.Selector.MatchLabelsEntry - nil, // 51: ateapi.Worker.LabelsEntry - (*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp - (*fieldmaskpb.FieldMask)(nil), // 53: google.protobuf.FieldMask + (ActorCertificatePurpose)(0), // 2: ateapi.ActorCertificatePurpose + (ExternalVolume_Status)(0), // 3: ateapi.ExternalVolume.Status + (Actor_Status)(0), // 4: ateapi.Actor.Status + (Worker_State)(0), // 5: ateapi.Worker.State + (*LocalSnapshotInfo)(nil), // 6: ateapi.LocalSnapshotInfo + (*Selector)(nil), // 7: ateapi.Selector + (*ResourceMetadata)(nil), // 8: ateapi.ResourceMetadata + (*ExternalVolume)(nil), // 9: ateapi.ExternalVolume + (*Actor)(nil), // 10: ateapi.Actor + (*WorkerAssignment)(nil), // 11: ateapi.WorkerAssignment + (*ActorSnapshot)(nil), // 12: ateapi.ActorSnapshot + (*ActorSnapshotTag)(nil), // 13: ateapi.ActorSnapshotTag + (*Atespace)(nil), // 14: ateapi.Atespace + (*ObjectRef)(nil), // 15: ateapi.ObjectRef + (*ActorSnapshotRef)(nil), // 16: ateapi.ActorSnapshotRef + (*CreateAtespaceRequest)(nil), // 17: ateapi.CreateAtespaceRequest + (*GetAtespaceRequest)(nil), // 18: ateapi.GetAtespaceRequest + (*ListAtespacesRequest)(nil), // 19: ateapi.ListAtespacesRequest + (*ListAtespacesResponse)(nil), // 20: ateapi.ListAtespacesResponse + (*DeleteAtespaceRequest)(nil), // 21: ateapi.DeleteAtespaceRequest + (*GetActorRequest)(nil), // 22: ateapi.GetActorRequest + (*CreateActorRequest)(nil), // 23: ateapi.CreateActorRequest + (*UpdateActorRequest)(nil), // 24: ateapi.UpdateActorRequest + (*SuspendActorRequest)(nil), // 25: ateapi.SuspendActorRequest + (*SuspendActorResponse)(nil), // 26: ateapi.SuspendActorResponse + (*PauseActorRequest)(nil), // 27: ateapi.PauseActorRequest + (*PauseActorResponse)(nil), // 28: ateapi.PauseActorResponse + (*ResumeActorRequest)(nil), // 29: ateapi.ResumeActorRequest + (*ResumeActorResponse)(nil), // 30: ateapi.ResumeActorResponse + (*DeleteActorRequest)(nil), // 31: ateapi.DeleteActorRequest + (*GetActorSnapshotRequest)(nil), // 32: ateapi.GetActorSnapshotRequest + (*ListActorSnapshotsRequest)(nil), // 33: ateapi.ListActorSnapshotsRequest + (*ListActorSnapshotsResponse)(nil), // 34: ateapi.ListActorSnapshotsResponse + (*TagActorSnapshotRequest)(nil), // 35: ateapi.TagActorSnapshotRequest + (*UpdateActorSnapshotTagRequest)(nil), // 36: ateapi.UpdateActorSnapshotTagRequest + (*DeleteActorSnapshotTagRequest)(nil), // 37: ateapi.DeleteActorSnapshotTagRequest + (*ListWorkersRequest)(nil), // 38: ateapi.ListWorkersRequest + (*ListWorkersResponse)(nil), // 39: ateapi.ListWorkersResponse + (*ListActorsRequest)(nil), // 40: ateapi.ListActorsRequest + (*ListActorsResponse)(nil), // 41: ateapi.ListActorsResponse + (*Worker)(nil), // 42: ateapi.Worker + (*Assignment)(nil), // 43: ateapi.Assignment + (*KubeNamespacedObjectRef)(nil), // 44: ateapi.KubeNamespacedObjectRef + (*DebugClearRequest)(nil), // 45: ateapi.DebugClearRequest + (*DebugClearResponse)(nil), // 46: ateapi.DebugClearResponse + (*MintJWTRequest)(nil), // 47: ateapi.MintJWTRequest + (*MintJWTResponse)(nil), // 48: ateapi.MintJWTResponse + (*MintCertRequest)(nil), // 49: ateapi.MintCertRequest + (*MintCertResponse)(nil), // 50: ateapi.MintCertResponse + nil, // 51: ateapi.Selector.MatchLabelsEntry + nil, // 52: ateapi.Worker.LabelsEntry + (*timestamppb.Timestamp)(nil), // 53: google.protobuf.Timestamp + (*fieldmaskpb.FieldMask)(nil), // 54: google.protobuf.FieldMask } var file_ateapi_proto_depIdxs = []int32{ - 50, // 0: ateapi.Selector.match_labels:type_name -> ateapi.Selector.MatchLabelsEntry - 52, // 1: ateapi.ResourceMetadata.create_time:type_name -> google.protobuf.Timestamp - 52, // 2: ateapi.ResourceMetadata.update_time:type_name -> google.protobuf.Timestamp - 2, // 3: ateapi.ExternalVolume.status:type_name -> ateapi.ExternalVolume.Status - 7, // 4: ateapi.Actor.metadata:type_name -> ateapi.ResourceMetadata - 3, // 5: ateapi.Actor.status:type_name -> ateapi.Actor.Status - 10, // 6: ateapi.Actor.worker_assignment:type_name -> ateapi.WorkerAssignment - 6, // 7: ateapi.Actor.worker_selector:type_name -> ateapi.Selector - 14, // 8: ateapi.Actor.latest_snapshot:type_name -> ateapi.ObjectRef - 5, // 9: ateapi.Actor.local_snapshot_info:type_name -> ateapi.LocalSnapshotInfo - 8, // 10: ateapi.Actor.actor_volumes:type_name -> ateapi.ExternalVolume - 7, // 11: ateapi.ActorSnapshot.metadata:type_name -> ateapi.ResourceMetadata - 14, // 12: ateapi.ActorSnapshot.source_actor:type_name -> ateapi.ObjectRef + 51, // 0: ateapi.Selector.match_labels:type_name -> ateapi.Selector.MatchLabelsEntry + 53, // 1: ateapi.ResourceMetadata.create_time:type_name -> google.protobuf.Timestamp + 53, // 2: ateapi.ResourceMetadata.update_time:type_name -> google.protobuf.Timestamp + 3, // 3: ateapi.ExternalVolume.status:type_name -> ateapi.ExternalVolume.Status + 8, // 4: ateapi.Actor.metadata:type_name -> ateapi.ResourceMetadata + 4, // 5: ateapi.Actor.status:type_name -> ateapi.Actor.Status + 11, // 6: ateapi.Actor.worker_assignment:type_name -> ateapi.WorkerAssignment + 7, // 7: ateapi.Actor.worker_selector:type_name -> ateapi.Selector + 15, // 8: ateapi.Actor.latest_snapshot:type_name -> ateapi.ObjectRef + 6, // 9: ateapi.Actor.local_snapshot_info:type_name -> ateapi.LocalSnapshotInfo + 9, // 10: ateapi.Actor.actor_volumes:type_name -> ateapi.ExternalVolume + 8, // 11: ateapi.ActorSnapshot.metadata:type_name -> ateapi.ResourceMetadata + 15, // 12: ateapi.ActorSnapshot.source_actor:type_name -> ateapi.ObjectRef 0, // 13: ateapi.ActorSnapshot.content_scope:type_name -> ateapi.SnapshotContentScope - 7, // 14: ateapi.ActorSnapshotTag.metadata:type_name -> ateapi.ResourceMetadata - 14, // 15: ateapi.ActorSnapshotTag.snapshot:type_name -> ateapi.ObjectRef + 8, // 14: ateapi.ActorSnapshotTag.metadata:type_name -> ateapi.ResourceMetadata + 15, // 15: ateapi.ActorSnapshotTag.snapshot:type_name -> ateapi.ObjectRef 1, // 16: ateapi.ActorSnapshotTag.scope:type_name -> ateapi.ActorSnapshotTagScope - 7, // 17: ateapi.Atespace.metadata:type_name -> ateapi.ResourceMetadata - 14, // 18: ateapi.ActorSnapshotRef.snapshot:type_name -> ateapi.ObjectRef - 14, // 19: ateapi.ActorSnapshotRef.tag:type_name -> ateapi.ObjectRef - 13, // 20: ateapi.CreateAtespaceRequest.atespace:type_name -> ateapi.Atespace - 14, // 21: ateapi.GetAtespaceRequest.atespace:type_name -> ateapi.ObjectRef - 13, // 22: ateapi.ListAtespacesResponse.atespaces:type_name -> ateapi.Atespace - 14, // 23: ateapi.DeleteAtespaceRequest.atespace:type_name -> ateapi.ObjectRef - 14, // 24: ateapi.GetActorRequest.actor:type_name -> ateapi.ObjectRef - 9, // 25: ateapi.CreateActorRequest.actor:type_name -> ateapi.Actor - 15, // 26: ateapi.CreateActorRequest.source_snapshot:type_name -> ateapi.ActorSnapshotRef - 9, // 27: ateapi.UpdateActorRequest.actor:type_name -> ateapi.Actor - 53, // 28: ateapi.UpdateActorRequest.update_mask:type_name -> google.protobuf.FieldMask - 14, // 29: ateapi.SuspendActorRequest.actor:type_name -> ateapi.ObjectRef - 9, // 30: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor - 14, // 31: ateapi.PauseActorRequest.actor:type_name -> ateapi.ObjectRef - 9, // 32: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor - 14, // 33: ateapi.ResumeActorRequest.actor:type_name -> ateapi.ObjectRef - 9, // 34: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor - 14, // 35: ateapi.DeleteActorRequest.actor:type_name -> ateapi.ObjectRef - 15, // 36: ateapi.GetActorSnapshotRequest.snapshot:type_name -> ateapi.ActorSnapshotRef - 11, // 37: ateapi.ListActorSnapshotsResponse.snapshots:type_name -> ateapi.ActorSnapshot - 15, // 38: ateapi.TagActorSnapshotRequest.snapshot:type_name -> ateapi.ActorSnapshotRef - 12, // 39: ateapi.TagActorSnapshotRequest.tag:type_name -> ateapi.ActorSnapshotTag - 14, // 40: ateapi.UpdateActorSnapshotTagRequest.tag:type_name -> ateapi.ObjectRef + 8, // 17: ateapi.Atespace.metadata:type_name -> ateapi.ResourceMetadata + 15, // 18: ateapi.ActorSnapshotRef.snapshot:type_name -> ateapi.ObjectRef + 15, // 19: ateapi.ActorSnapshotRef.tag:type_name -> ateapi.ObjectRef + 14, // 20: ateapi.CreateAtespaceRequest.atespace:type_name -> ateapi.Atespace + 15, // 21: ateapi.GetAtespaceRequest.atespace:type_name -> ateapi.ObjectRef + 14, // 22: ateapi.ListAtespacesResponse.atespaces:type_name -> ateapi.Atespace + 15, // 23: ateapi.DeleteAtespaceRequest.atespace:type_name -> ateapi.ObjectRef + 15, // 24: ateapi.GetActorRequest.actor:type_name -> ateapi.ObjectRef + 10, // 25: ateapi.CreateActorRequest.actor:type_name -> ateapi.Actor + 16, // 26: ateapi.CreateActorRequest.source_snapshot:type_name -> ateapi.ActorSnapshotRef + 10, // 27: ateapi.UpdateActorRequest.actor:type_name -> ateapi.Actor + 54, // 28: ateapi.UpdateActorRequest.update_mask:type_name -> google.protobuf.FieldMask + 15, // 29: ateapi.SuspendActorRequest.actor:type_name -> ateapi.ObjectRef + 10, // 30: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor + 15, // 31: ateapi.PauseActorRequest.actor:type_name -> ateapi.ObjectRef + 10, // 32: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor + 15, // 33: ateapi.ResumeActorRequest.actor:type_name -> ateapi.ObjectRef + 10, // 34: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor + 15, // 35: ateapi.DeleteActorRequest.actor:type_name -> ateapi.ObjectRef + 16, // 36: ateapi.GetActorSnapshotRequest.snapshot:type_name -> ateapi.ActorSnapshotRef + 12, // 37: ateapi.ListActorSnapshotsResponse.snapshots:type_name -> ateapi.ActorSnapshot + 16, // 38: ateapi.TagActorSnapshotRequest.snapshot:type_name -> ateapi.ActorSnapshotRef + 13, // 39: ateapi.TagActorSnapshotRequest.tag:type_name -> ateapi.ActorSnapshotTag + 15, // 40: ateapi.UpdateActorSnapshotTagRequest.tag:type_name -> ateapi.ObjectRef 1, // 41: ateapi.UpdateActorSnapshotTagRequest.scope:type_name -> ateapi.ActorSnapshotTagScope - 14, // 42: ateapi.DeleteActorSnapshotTagRequest.tag:type_name -> ateapi.ObjectRef - 41, // 43: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker - 9, // 44: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor - 42, // 45: ateapi.Worker.assignment:type_name -> ateapi.Assignment - 51, // 46: ateapi.Worker.labels:type_name -> ateapi.Worker.LabelsEntry - 4, // 47: ateapi.Worker.state:type_name -> ateapi.Worker.State - 43, // 48: ateapi.Assignment.actor_template:type_name -> ateapi.KubeNamespacedObjectRef - 14, // 49: ateapi.Assignment.actor:type_name -> ateapi.ObjectRef - 21, // 50: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest - 22, // 51: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest - 23, // 52: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest - 24, // 53: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest - 26, // 54: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest - 28, // 55: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest - 30, // 56: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest - 31, // 57: ateapi.Control.GetActorSnapshot:input_type -> ateapi.GetActorSnapshotRequest - 32, // 58: ateapi.Control.ListActorSnapshots:input_type -> ateapi.ListActorSnapshotsRequest - 34, // 59: ateapi.Control.TagActorSnapshot:input_type -> ateapi.TagActorSnapshotRequest - 35, // 60: ateapi.Control.UpdateActorSnapshotTag:input_type -> ateapi.UpdateActorSnapshotTagRequest - 36, // 61: ateapi.Control.DeleteActorSnapshotTag:input_type -> ateapi.DeleteActorSnapshotTagRequest - 37, // 62: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest - 39, // 63: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest - 16, // 64: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest - 17, // 65: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest - 18, // 66: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest - 20, // 67: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest - 44, // 68: ateapi.Debug.DebugClear:input_type -> ateapi.DebugClearRequest - 46, // 69: ateapi.ActorIdentity.MintJWT:input_type -> ateapi.MintJWTRequest - 48, // 70: ateapi.ActorIdentity.MintCert:input_type -> ateapi.MintCertRequest - 9, // 71: ateapi.Control.GetActor:output_type -> ateapi.Actor - 9, // 72: ateapi.Control.CreateActor:output_type -> ateapi.Actor - 9, // 73: ateapi.Control.UpdateActor:output_type -> ateapi.Actor - 25, // 74: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse - 27, // 75: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse - 29, // 76: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse - 9, // 77: ateapi.Control.DeleteActor:output_type -> ateapi.Actor - 11, // 78: ateapi.Control.GetActorSnapshot:output_type -> ateapi.ActorSnapshot - 33, // 79: ateapi.Control.ListActorSnapshots:output_type -> ateapi.ListActorSnapshotsResponse - 12, // 80: ateapi.Control.TagActorSnapshot:output_type -> ateapi.ActorSnapshotTag - 12, // 81: ateapi.Control.UpdateActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag - 12, // 82: ateapi.Control.DeleteActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag - 38, // 83: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse - 40, // 84: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse - 13, // 85: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace - 13, // 86: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace - 19, // 87: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse - 13, // 88: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace - 45, // 89: ateapi.Debug.DebugClear:output_type -> ateapi.DebugClearResponse - 47, // 90: ateapi.ActorIdentity.MintJWT:output_type -> ateapi.MintJWTResponse - 49, // 91: ateapi.ActorIdentity.MintCert:output_type -> ateapi.MintCertResponse - 71, // [71:92] is the sub-list for method output_type - 50, // [50:71] is the sub-list for method input_type - 50, // [50:50] is the sub-list for extension type_name - 50, // [50:50] is the sub-list for extension extendee - 0, // [0:50] is the sub-list for field type_name + 15, // 42: ateapi.DeleteActorSnapshotTagRequest.tag:type_name -> ateapi.ObjectRef + 42, // 43: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker + 10, // 44: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor + 43, // 45: ateapi.Worker.assignment:type_name -> ateapi.Assignment + 52, // 46: ateapi.Worker.labels:type_name -> ateapi.Worker.LabelsEntry + 5, // 47: ateapi.Worker.state:type_name -> ateapi.Worker.State + 44, // 48: ateapi.Assignment.actor_template:type_name -> ateapi.KubeNamespacedObjectRef + 15, // 49: ateapi.Assignment.actor:type_name -> ateapi.ObjectRef + 2, // 50: ateapi.MintCertRequest.purpose:type_name -> ateapi.ActorCertificatePurpose + 22, // 51: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest + 23, // 52: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest + 24, // 53: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest + 25, // 54: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest + 27, // 55: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest + 29, // 56: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest + 31, // 57: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest + 32, // 58: ateapi.Control.GetActorSnapshot:input_type -> ateapi.GetActorSnapshotRequest + 33, // 59: ateapi.Control.ListActorSnapshots:input_type -> ateapi.ListActorSnapshotsRequest + 35, // 60: ateapi.Control.TagActorSnapshot:input_type -> ateapi.TagActorSnapshotRequest + 36, // 61: ateapi.Control.UpdateActorSnapshotTag:input_type -> ateapi.UpdateActorSnapshotTagRequest + 37, // 62: ateapi.Control.DeleteActorSnapshotTag:input_type -> ateapi.DeleteActorSnapshotTagRequest + 38, // 63: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest + 40, // 64: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest + 17, // 65: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest + 18, // 66: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest + 19, // 67: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest + 21, // 68: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest + 45, // 69: ateapi.Debug.DebugClear:input_type -> ateapi.DebugClearRequest + 47, // 70: ateapi.ActorIdentity.MintJWT:input_type -> ateapi.MintJWTRequest + 49, // 71: ateapi.ActorIdentity.MintCert:input_type -> ateapi.MintCertRequest + 10, // 72: ateapi.Control.GetActor:output_type -> ateapi.Actor + 10, // 73: ateapi.Control.CreateActor:output_type -> ateapi.Actor + 10, // 74: ateapi.Control.UpdateActor:output_type -> ateapi.Actor + 26, // 75: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse + 28, // 76: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse + 30, // 77: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse + 10, // 78: ateapi.Control.DeleteActor:output_type -> ateapi.Actor + 12, // 79: ateapi.Control.GetActorSnapshot:output_type -> ateapi.ActorSnapshot + 34, // 80: ateapi.Control.ListActorSnapshots:output_type -> ateapi.ListActorSnapshotsResponse + 13, // 81: ateapi.Control.TagActorSnapshot:output_type -> ateapi.ActorSnapshotTag + 13, // 82: ateapi.Control.UpdateActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag + 13, // 83: ateapi.Control.DeleteActorSnapshotTag:output_type -> ateapi.ActorSnapshotTag + 39, // 84: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse + 41, // 85: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse + 14, // 86: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace + 14, // 87: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace + 20, // 88: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse + 14, // 89: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace + 46, // 90: ateapi.Debug.DebugClear:output_type -> ateapi.DebugClearResponse + 48, // 91: ateapi.ActorIdentity.MintJWT:output_type -> ateapi.MintJWTResponse + 50, // 92: ateapi.ActorIdentity.MintCert:output_type -> ateapi.MintCertResponse + 72, // [72:93] is the sub-list for method output_type + 51, // [51:72] is the sub-list for method input_type + 51, // [51:51] is the sub-list for extension type_name + 51, // [51:51] is the sub-list for extension extendee + 0, // [0:51] is the sub-list for field type_name } func init() { file_ateapi_proto_init() } @@ -3393,7 +3467,7 @@ func file_ateapi_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ateapi_proto_rawDesc), len(file_ateapi_proto_rawDesc)), - NumEnums: 5, + NumEnums: 6, NumMessages: 47, NumExtensions: 0, NumServices: 3, diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index bc576cfc9..fa886550d 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -481,20 +481,10 @@ message DebugClearResponse {} // ActorIdentity allows substrate workloads to exchange their // infrastructure-level credentials (k8s service account token, etc.) for a -// substrate actor-level credential. A given substrate actor might migrate +// substrate actor-level credential. A given substrate actor might migrate // between many different physical workers over the course of its lifecycle, // whereas the actor credential's identity will be stable for the life of the // actor. -// -// This service requires authentication. You can authenticate with a Kubernetes -// service account token in an `Authorization: Bearer` header, or you can -// authenticate with a Kubernetes service account certificate as an mTLS -// certificate. (Kubernetes service account certificates do not currently exist -// upstream, but we will provide a polyfill based on Pod Certificates). -// -// The broker will check that the service credentials you authenticated with -// belong to a Pod that is currently mapped to the requested actor in the -// actor database. service ActorIdentity { // Request an Actor Identity JWT. // @@ -508,10 +498,9 @@ service ActorIdentity { // it on the actor's behalf, authenticating with its own client certificate // rather than a bearer token. // - // Authorization is decided on that client certificate: it must identify the - // atelet running on the same node as the worker Pod that currently hosts the - // requested actor, and the actor must still be running. Any other caller is - // rejected with PERMISSION_DENIED. + // Authorization is decided on that client certificate and the worker + // identity attested by atelet. Ateapi verifies that the worker is assigned to + // the actor and that the actor points back to that exact worker before signing. // // The certificate in the response is the actor's identity, not the atelet's. rpc MintCert(MintCertRequest) returns (MintCertResponse); @@ -549,14 +538,28 @@ message MintJWTResponse { } message MintCertRequest { - string atespace = 1; - string actor_name = 2; - string actor_uid = 3; + // Worker identity authenticated by the node-local atelet. Ateapi resolves + // the worker's current actor assignment rather than trusting actor metadata + // from the caller. + string worker_namespace = 1; + string worker_pod = 2; + string worker_pod_uid = 3; // Request contains DER encoded bytes of a x509 certificate signing request. // The signer will ignore the contents of the CSR except to extract the // subject public key. bytes certificate_signing_request = 4; + + // Actor incarnation expected by the activation. This is only a stale-request + // guard: ateapi derives the actor and its identity from the worker assignment. + string expected_actor_uid = 5; + + ActorCertificatePurpose purpose = 6; +} + +enum ActorCertificatePurpose { + ACTOR_CERTIFICATE_PURPOSE_UNSPECIFIED = 0; + ACTOR_CERTIFICATE_PURPOSE_ATUNNEL = 1; } message MintCertResponse { diff --git a/pkg/proto/ateapipb/ateapi_grpc.pb.go b/pkg/proto/ateapipb/ateapi_grpc.pb.go index 02ad118ca..569481fd3 100644 --- a/pkg/proto/ateapipb/ateapi_grpc.pb.go +++ b/pkg/proto/ateapipb/ateapi_grpc.pb.go @@ -945,20 +945,10 @@ const ( // // ActorIdentity allows substrate workloads to exchange their // infrastructure-level credentials (k8s service account token, etc.) for a -// substrate actor-level credential. A given substrate actor might migrate +// substrate actor-level credential. A given substrate actor might migrate // between many different physical workers over the course of its lifecycle, // whereas the actor credential's identity will be stable for the life of the // actor. -// -// This service requires authentication. You can authenticate with a Kubernetes -// service account token in an `Authorization: Bearer` header, or you can -// authenticate with a Kubernetes service account certificate as an mTLS -// certificate. (Kubernetes service account certificates do not currently exist -// upstream, but we will provide a polyfill based on Pod Certificates). -// -// The broker will check that the service credentials you authenticated with -// belong to a Pod that is currently mapped to the requested actor in the -// actor database. type ActorIdentityClient interface { // Request an Actor Identity JWT. // @@ -971,10 +961,9 @@ type ActorIdentityClient interface { // it on the actor's behalf, authenticating with its own client certificate // rather than a bearer token. // - // Authorization is decided on that client certificate: it must identify the - // atelet running on the same node as the worker Pod that currently hosts the - // requested actor, and the actor must still be running. Any other caller is - // rejected with PERMISSION_DENIED. + // Authorization is decided on that client certificate and the worker + // identity attested by atelet. Ateapi verifies that the worker is assigned to + // the actor and that the actor points back to that exact worker before signing. // // The certificate in the response is the actor's identity, not the atelet's. MintCert(ctx context.Context, in *MintCertRequest, opts ...grpc.CallOption) (*MintCertResponse, error) @@ -1014,20 +1003,10 @@ func (c *actorIdentityClient) MintCert(ctx context.Context, in *MintCertRequest, // // ActorIdentity allows substrate workloads to exchange their // infrastructure-level credentials (k8s service account token, etc.) for a -// substrate actor-level credential. A given substrate actor might migrate +// substrate actor-level credential. A given substrate actor might migrate // between many different physical workers over the course of its lifecycle, // whereas the actor credential's identity will be stable for the life of the // actor. -// -// This service requires authentication. You can authenticate with a Kubernetes -// service account token in an `Authorization: Bearer` header, or you can -// authenticate with a Kubernetes service account certificate as an mTLS -// certificate. (Kubernetes service account certificates do not currently exist -// upstream, but we will provide a polyfill based on Pod Certificates). -// -// The broker will check that the service credentials you authenticated with -// belong to a Pod that is currently mapped to the requested actor in the -// actor database. type ActorIdentityServer interface { // Request an Actor Identity JWT. // @@ -1040,10 +1019,9 @@ type ActorIdentityServer interface { // it on the actor's behalf, authenticating with its own client certificate // rather than a bearer token. // - // Authorization is decided on that client certificate: it must identify the - // atelet running on the same node as the worker Pod that currently hosts the - // requested actor, and the actor must still be running. Any other caller is - // rejected with PERMISSION_DENIED. + // Authorization is decided on that client certificate and the worker + // identity attested by atelet. Ateapi verifies that the worker is assigned to + // the actor and that the actor points back to that exact worker before signing. // // The certificate in the response is the actor's identity, not the atelet's. MintCert(context.Context, *MintCertRequest) (*MintCertResponse, error)