Skip to content

Commit 088381b

Browse files
atenet: Router checks the routed worker's SPIFFE ID on atunnel's cert (#2678)
Part of #2078 (Envoy dataplane, @EItanya @howardjohn for agw) The router accepted any podidentity cert in the trust domain as atunnel, so any pod in the cluster could pose as a worker. It now requires the exact SPIFFE ID of the worker each request is routed to. - atecontroller's syncer records the worker pod's SPIFFE ID on the Worker (new `worker_spiffe_id`, built from the pod's namespace and ServiceAccount), and ateapi copies it into the actor's worker assignment. ateapi rejects a value that isn't a valid SPIFFE ID. - ext_proc passes that ID to Envoy as is and refuses to route without it. Envoy requires that exact SAN, on both the :443 and CONNECT paths. `--upstream-spiffe-prefix` is removed; the cluster still checks the trust domain. - The Worker carries the SPIFFE ID rather than the ServiceAccount, so the router doesn't need to know how the ID is built. A different ID scheme only changes atecontroller. - WorkerPools that don't set serviceAccountName now run as an `ateworker` ServiceAccount the controller creates, instead of `default`, so the worker's ID isn't shared with every other pod in the namespace. Remaining gap: another pod running as the same ServiceAccount in the same namespace can still pose as the worker. Closing it needs a signer that puts the pod in the worker's SPIFFE ID; atecontroller would then record that ID, with no router change. #### Breaking change - The router rejects workers registered before this change until their pods are replaced. Pools on the default ServiceAccount roll automatically; pools that set `spec.template.serviceAccountName` need a restart. Upgrade atecontroller and ate-api-server before atenet-router. - Workers now run as an `ateworker` ServiceAccount by default instead of `default`. RBAC, Workload Identity bindings and imagePullSecrets attached to `default` no longer apply to them; set `serviceAccountName: default` or move those to `ateworker`. - atenet-router no longer has `--upstream-spiffe-prefix`. - atecontroller now needs create and patch on serviceaccounts (included in the generated role). > It's a good idea to open an issue first for discussion. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR
1 parent ac296df commit 088381b

31 files changed

Lines changed: 485 additions & 71 deletions

‎cmd/ateapi/internal/apivalidation/actor.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ func ValidateCustom_WorkerAssignment_WorkerPodIps(_ context.Context, _ operation
138138
return validateDualStackIPs(fldPath, value)
139139
}
140140

141+
// This is needed because DV doesn't have a format for SPIFFE IDs.
142+
func ValidateCustom_WorkerAssignment_WorkerSpiffeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
143+
return validateSPIFFEID(fldPath, *value)
144+
}
145+
141146
func ValidateCustom_ExternalVolume_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
142147
return resources.ValidateVolumeType(fldPath, *value)
143148
}

‎cmd/ateapi/internal/apivalidation/actor_test.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,21 @@ func TestValidateActorUpdate(t *testing.T) {
354354
want: field.ErrorList{
355355
field.TooMany(field.NewPath("status", "worker_assignment", "worker_pod_ips"), 3, 2).WithOrigin("maxItems"),
356356
},
357+
}, {
358+
name: "set valid actor.status.worker_assignment.worker_spiffe_id",
359+
oldVal: validInput(withStatus()),
360+
newVal: validOutput(withStatus(withWorkerAssignment(func(wa *ateapipb.WorkerAssignment) {
361+
wa.WorkerSpiffeId = "spiffe://cluster.local/ns/ns/sa/ateworker"
362+
}))),
363+
}, {
364+
name: "invalid actor.status.worker_assignment.worker_spiffe_id",
365+
oldVal: validInput(withStatus()),
366+
newVal: validOutput(withStatus(withWorkerAssignment(func(wa *ateapipb.WorkerAssignment) {
367+
wa.WorkerSpiffeId = "ns/ns/sa/ateworker"
368+
}))),
369+
want: field.ErrorList{
370+
field.Invalid(field.NewPath("status", "worker_assignment", "worker_spiffe_id"), nil, ""),
371+
},
357372
}, {
358373
name: "clear actor.status.worker_assignment",
359374
oldVal: validInput(withStatus(withWorkerAssignment())),

‎cmd/ateapi/internal/apivalidation/common.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/agent-substrate/substrate/internal/preview"
2323
"github.com/agent-substrate/substrate/internal/resources"
2424
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
25+
"github.com/spiffe/go-spiffe/v2/spiffeid"
2526
"k8s.io/apimachinery/pkg/api/operation"
2627
"k8s.io/apimachinery/pkg/util/validation"
2728
"k8s.io/apimachinery/pkg/util/validation/field"
@@ -119,3 +120,12 @@ func validateDualStackIPs(fldPath *field.Path, ips []string) field.ErrorList {
119120
}
120121
return errs
121122
}
123+
124+
// validateSPIFFEID checks that id is a SPIFFE ID. Presence and length are
125+
// enforced by tags.
126+
func validateSPIFFEID(fldPath *field.Path, id string) field.ErrorList {
127+
if _, err := spiffeid.FromString(id); err != nil {
128+
return field.ErrorList{field.Invalid(fldPath, id, err.Error())}
129+
}
130+
return nil
131+
}

‎cmd/ateapi/internal/apivalidation/worker.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ func ValidateCustom_Worker_Ips(_ context.Context, _ operation.Operation, fldPath
134134
return validateDualStackIPs(fldPath, value)
135135
}
136136

137+
// This is needed because DV doesn't have a format for SPIFFE IDs.
138+
func ValidateCustom_Worker_WorkerSpiffeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
139+
return validateSPIFFEID(fldPath, *value)
140+
}
141+
137142
// epoch only moves forward: a lower value would make Actors placed during the
138143
// current run look older than it, and crash them.
139144
func ValidateCustom_Worker_Epoch(_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *int64) field.ErrorList {

‎cmd/ateapi/internal/apivalidation/worker_test.go‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,23 @@ func TestValidateCreateWorkerRequest(t *testing.T) {
142142
w.Ips = []string{"10.1.2.3", "fd00::1", "10.1.2.4"}
143143
})),
144144
want: field.ErrorList{field.TooMany(field.NewPath("worker", "ips"), 3, 2).WithOrigin("maxItems")},
145+
}, {
146+
name: "valid worker_spiffe_id",
147+
req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) {
148+
w.WorkerSpiffeId = "spiffe://cluster.local/ns/ate-system/sa/ateworker"
149+
})),
150+
}, {
151+
name: "invalid worker_spiffe_id",
152+
req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) {
153+
w.WorkerSpiffeId = "https://cluster.local/ns/ate-system/sa/ateworker"
154+
})),
155+
want: field.ErrorList{field.Invalid(field.NewPath("worker", "worker_spiffe_id"), nil, "")},
156+
}, {
157+
name: "worker_spiffe_id too long",
158+
req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) {
159+
w.WorkerSpiffeId = "spiffe://cluster.local/" + strings.Repeat("a", 2048)
160+
})),
161+
want: field.ErrorList{field.TooLong(field.NewPath("worker", "worker_spiffe_id"), nil, 2048).WithOrigin("maxLength")},
145162
}, {
146163
name: "sandbox_class too long",
147164
req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.SandboxClass = strings.Repeat("x", 64) })),

‎cmd/ateapi/internal/apivalidation/zz_generated.validation.go‎

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cmd/ateapi/internal/controlapi/workflow_resume.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ func workerAssignmentFrom(w *ateapipb.Worker, snapRuntime *ateapipb.SandboxRunti
610610
WorkerPodUid: w.GetWorkerPodUid(),
611611
WorkerPodIps: slices.Clone(w.GetIps()),
612612
NodeName: w.GetNodeName(),
613+
WorkerSpiffeId: w.GetWorkerSpiffeId(),
613614
// The runtime the actor is started or restored under. Snapshots taken
614615
// during this assignment are stamped with it, so a worker that later
615616
// changes its default does not relabel state captured on the old one.

‎cmd/ateapi/internal/controlapi/workflow_resume_test.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,3 +2689,13 @@ func TestCanRelaxRuntime(t *testing.T) {
26892689
})
26902690
}
26912691
}
2692+
2693+
// atenet-router requires the assignment's SPIFFE ID on atunnel's certificate,
2694+
// so the worker's has to reach it.
2695+
func TestWorkerAssignmentFromCopiesSPIFFEID(t *testing.T) {
2696+
const id = "spiffe://cluster.local/ns/workers/sa/substrate-worker"
2697+
got := workerAssignmentFrom(&ateapipb.Worker{WorkerSpiffeId: id}, nil)
2698+
if got.GetWorkerSpiffeId() != id {
2699+
t.Errorf("worker SPIFFE ID = %q, want %q", got.GetWorkerSpiffeId(), id)
2700+
}
2701+
}

‎cmd/atecontroller/internal/controllers/workerpool_apply.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ func applyWorkerPoolPodTemplate(
551551
podSpecAC.NodeSelector = map[string]string{}
552552
podSpecAC.Tolerations = []corev1ac.TolerationApplyConfiguration{}
553553
podSpecAC.WithPriorityClassName("")
554-
podSpecAC.WithServiceAccountName("default")
554+
podSpecAC.WithServiceAccountName(installdefaults.WorkerServiceAccount)
555555
podSpecAC.WithAffinity(corev1ac.Affinity())
556556
resourcesAC := corev1ac.ResourceRequirements()
557557
containerAC.WithResources(resourcesAC)

‎cmd/atecontroller/internal/controllers/workerpool_apply_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@ func expectedDeploymentApplyConfig(mutatePodSpec func(*corev1ac.PodSpecApplyConf
905905
sandboxClassTolerationAC(atev1alpha1.SandboxClassGvisor),
906906
}
907907
podSpecAC.WithPriorityClassName("")
908-
podSpecAC.WithServiceAccountName("default")
908+
podSpecAC.WithServiceAccountName("ateworker")
909909
podSpecAC.WithAffinity(corev1ac.Affinity())
910910
podSpecAC.WithTerminationGracePeriodSeconds(workerTerminationGracePeriodSeconds)
911911
if mutatePodSpec != nil {

0 commit comments

Comments
 (0)