From d1faa162f1c35a0439504610c128f786277df6bd Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 4 Jul 2026 11:37:48 +0100 Subject: [PATCH 1/3] server: k8s_psat: validate pod UID TokenReview authenticates the projected service account token and returns the pod UID bound to that token. The server then fetches the live pod by namespace and name to derive node identity, selectors, and the agent SPIFFE ID. Reject the attestation when the fetched pod UID does not match the TokenReview pod UID so a stale or raced pod lookup cannot bind authenticated token data to a different live pod. Update the unit tests to model pod UIDs explicitly and cover the mismatch case. Signed-off-by: Carlo Teubner --- .../plugin/nodeattestor/k8spsat/psat.go | 3 +++ .../plugin/nodeattestor/k8spsat/psat_test.go | 26 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pkg/server/plugin/nodeattestor/k8spsat/psat.go b/pkg/server/plugin/nodeattestor/k8spsat/psat.go index 92d4f29612..a876e43e16 100644 --- a/pkg/server/plugin/nodeattestor/k8spsat/psat.go +++ b/pkg/server/plugin/nodeattestor/k8spsat/psat.go @@ -224,6 +224,9 @@ func (p *AttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) if err != nil { return status.Errorf(codes.Internal, "fail to get pod from k8s API server for cluster %q: %v", attestationData.Cluster, err) } + if string(pod.UID) != podUID { + return status.Errorf(codes.Internal, "pod UID mismatch for cluster %q", attestationData.Cluster) + } node, err := cluster.client.GetNode(stream.Context(), pod.Spec.NodeName) if err != nil { diff --git a/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go b/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go index a6a16278b4..82b3f165f6 100644 --- a/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go +++ b/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go @@ -242,7 +242,7 @@ func (s *AttestorSuite) TestAttestFailsIfCannotGetNode() { } token := s.signToken(s.fooSigner, tokenData) s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) - s.apiServerClient.SetPod(createPod("NS1", "PODNAME", "NODENAME", "172.16.0.1")) + s.apiServerClient.SetPod(createPod("NS1", "PODNAME", "PODUID", "NODENAME", "172.16.0.1")) s.requireAttestError(makePayload("FOO", token), codes.Internal, "nodeattestor(k8s_psat): fail to get node from k8s API server") @@ -257,13 +257,28 @@ func (s *AttestorSuite) TestAttestFailsIfNodeUIDIsEmpty() { } token := s.signToken(s.fooSigner, tokenData) s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) - s.apiServerClient.SetPod(createPod("NS1", "PODNAME", "NODENAME", "172.16.0.1")) + s.apiServerClient.SetPod(createPod("NS1", "PODNAME", "PODUID", "NODENAME", "172.16.0.1")) s.apiServerClient.SetNode(createNode("NODENAME", "")) s.requireAttestError(makePayload("FOO", token), codes.Internal, "node UID is empty") } +func (s *AttestorSuite) TestAttestFailsIfPodUIDDoesNotMatchTokenStatus() { + tokenData := &TokenData{ + namespace: "NS1", + serviceAccountName: "SA1", + podName: "PODNAME", + podUID: "PODUID", + } + token := s.signToken(s.fooSigner, tokenData) + s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) + s.apiServerClient.SetPod(createPod("NS1", "PODNAME", "OTHER-PODUID", "NODENAME", "172.16.0.1")) + s.requireAttestError(makePayload("FOO", token), + codes.Internal, + "pod UID mismatch") +} + func (s *AttestorSuite) TestAttestSuccess() { // Success with FOO signed token tokenData := &TokenData{ @@ -274,7 +289,7 @@ func (s *AttestorSuite) TestAttestSuccess() { } token := s.signToken(s.fooSigner, tokenData) s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) - s.apiServerClient.SetPod(createPod("NS1", "PODNAME-1", "NODENAME-1", "172.16.10.1")) + s.apiServerClient.SetPod(createPod("NS1", "PODNAME-1", "PODUID-1", "NODENAME-1", "172.16.10.1")) s.apiServerClient.SetNode(createNode("NODENAME-1", "NODEUID-1")) result, err := s.attestor.Attest(context.Background(), makePayload("FOO", token), expectNoChallenge) @@ -303,7 +318,7 @@ func (s *AttestorSuite) TestAttestSuccess() { } token = s.signToken(s.barSigner, tokenData) s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, []string{"AUDIENCE"})) - s.apiServerClient.SetPod(createPod("NS2", "PODNAME-2", "NODENAME-2", "172.16.10.2")) + s.apiServerClient.SetPod(createPod("NS2", "PODNAME-2", "PODUID-2", "NODENAME-2", "172.16.10.2")) s.apiServerClient.SetNode(createNode("NODENAME-2", "NODEUID-2")) // Success with BAR signed token @@ -462,11 +477,12 @@ func createTokenStatus(tokenData *TokenData, authenticated bool, audience []stri } } -func createPod(namespace, podName, nodeName string, hostIP string) *corev1.Pod { +func createPod(namespace, podName, podUID, nodeName, hostIP string) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: podName, + UID: types.UID(podUID), Labels: map[string]string{ "PODLABEL-A": "A", "PODLABEL-B": "B", From d4bc0fc3629b4420b73a66fb4d73ab78598da70d Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 4 Jul 2026 12:49:49 +0100 Subject: [PATCH 2/3] server: k8s_psat: address Copilot comment Signed-off-by: Carlo Teubner --- pkg/server/plugin/nodeattestor/k8spsat/psat.go | 2 +- pkg/server/plugin/nodeattestor/k8spsat/psat_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/server/plugin/nodeattestor/k8spsat/psat.go b/pkg/server/plugin/nodeattestor/k8spsat/psat.go index a876e43e16..f8572d2a0c 100644 --- a/pkg/server/plugin/nodeattestor/k8spsat/psat.go +++ b/pkg/server/plugin/nodeattestor/k8spsat/psat.go @@ -225,7 +225,7 @@ func (p *AttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) return status.Errorf(codes.Internal, "fail to get pod from k8s API server for cluster %q: %v", attestationData.Cluster, err) } if string(pod.UID) != podUID { - return status.Errorf(codes.Internal, "pod UID mismatch for cluster %q", attestationData.Cluster) + return status.Errorf(codes.PermissionDenied, "pod UID mismatch for cluster %q", attestationData.Cluster) } node, err := cluster.client.GetNode(stream.Context(), pod.Spec.NodeName) diff --git a/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go b/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go index 82b3f165f6..d05c3adb6e 100644 --- a/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go +++ b/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go @@ -275,7 +275,7 @@ func (s *AttestorSuite) TestAttestFailsIfPodUIDDoesNotMatchTokenStatus() { s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) s.apiServerClient.SetPod(createPod("NS1", "PODNAME", "OTHER-PODUID", "NODENAME", "172.16.0.1")) s.requireAttestError(makePayload("FOO", token), - codes.Internal, + codes.PermissionDenied, "pod UID mismatch") } From 2b5bbe51723ec8ed2360279d55dcc54f80334dd9 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 4 Jul 2026 12:55:30 +0100 Subject: [PATCH 3/3] server: k8s_psat: fix error message grammar Signed-off-by: Carlo Teubner --- pkg/server/plugin/nodeattestor/k8spsat/psat.go | 10 +++++----- pkg/server/plugin/nodeattestor/k8spsat/psat_test.go | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/server/plugin/nodeattestor/k8spsat/psat.go b/pkg/server/plugin/nodeattestor/k8spsat/psat.go index f8572d2a0c..8d726c605c 100644 --- a/pkg/server/plugin/nodeattestor/k8spsat/psat.go +++ b/pkg/server/plugin/nodeattestor/k8spsat/psat.go @@ -202,7 +202,7 @@ func (p *AttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) namespace, serviceAccountName, err := k8s.GetNamesFromTokenStatus(tokenStatus) if err != nil { - return status.Errorf(codes.Internal, "fail to parse username from token review status for cluster %q: %v", attestationData.Cluster, err) + return status.Errorf(codes.Internal, "failed to parse username from token review status for cluster %q: %v", attestationData.Cluster, err) } fullServiceAccountName := fmt.Sprintf("%v:%v", namespace, serviceAccountName) @@ -212,17 +212,17 @@ func (p *AttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) podName, err := k8s.GetPodNameFromTokenStatus(tokenStatus) if err != nil { - return status.Errorf(codes.Internal, "fail to get pod name from token review status for cluster %q: %v", attestationData.Cluster, err) + return status.Errorf(codes.Internal, "failed to get pod name from token review status for cluster %q: %v", attestationData.Cluster, err) } podUID, err := k8s.GetPodUIDFromTokenStatus(tokenStatus) if err != nil { - return status.Errorf(codes.Internal, "fail to get pod UID from token review status for cluster %q: %v", attestationData.Cluster, err) + return status.Errorf(codes.Internal, "failed to get pod UID from token review status for cluster %q: %v", attestationData.Cluster, err) } pod, err := cluster.client.GetPod(stream.Context(), namespace, podName) if err != nil { - return status.Errorf(codes.Internal, "fail to get pod from k8s API server for cluster %q: %v", attestationData.Cluster, err) + return status.Errorf(codes.Internal, "failed to get pod from k8s API server for cluster %q: %v", attestationData.Cluster, err) } if string(pod.UID) != podUID { return status.Errorf(codes.PermissionDenied, "pod UID mismatch for cluster %q", attestationData.Cluster) @@ -230,7 +230,7 @@ func (p *AttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServer) node, err := cluster.client.GetNode(stream.Context(), pod.Spec.NodeName) if err != nil { - return status.Errorf(codes.Internal, "fail to get node from k8s API server for cluster %q: %v", attestationData.Cluster, err) + return status.Errorf(codes.Internal, "failed to get node from k8s API server for cluster %q: %v", attestationData.Cluster, err) } nodeUID := string(node.UID) diff --git a/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go b/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go index d05c3adb6e..e4ce7ed075 100644 --- a/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go +++ b/pkg/server/plugin/nodeattestor/k8spsat/psat_test.go @@ -163,7 +163,7 @@ func (s *AttestorSuite) TestAttestFailsWithMissingNamespaceClaim() { s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) s.requireAttestError(makePayload("FOO", token), codes.Internal, - "nodeattestor(k8s_psat): fail to parse username from token review status") + "nodeattestor(k8s_psat): failed to parse username from token review status") } func (s *AttestorSuite) TestAttestFailsWithMissingServiceAccountNameClaim() { @@ -176,7 +176,7 @@ func (s *AttestorSuite) TestAttestFailsWithMissingServiceAccountNameClaim() { s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) s.requireAttestError(makePayload("FOO", token), codes.Internal, - "nodeattestor(k8s_psat): fail to parse username from token review status") + "nodeattestor(k8s_psat): failed to parse username from token review status") } func (s *AttestorSuite) TestAttestFailsWithMissingPodNameClaim() { @@ -189,7 +189,7 @@ func (s *AttestorSuite) TestAttestFailsWithMissingPodNameClaim() { s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) s.requireAttestError(makePayload("FOO", token), codes.Internal, - "nodeattestor(k8s_psat): fail to get pod name from token review status") + "nodeattestor(k8s_psat): failed to get pod name from token review status") } func (s *AttestorSuite) TestAttestFailsWithMissingPodUIDClaim() { @@ -202,7 +202,7 @@ func (s *AttestorSuite) TestAttestFailsWithMissingPodUIDClaim() { s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) s.requireAttestError(makePayload("FOO", token), codes.Internal, - "nodeattestor(k8s_psat): fail to get pod UID from token review status") + "nodeattestor(k8s_psat): failed to get pod UID from token review status") } func (s *AttestorSuite) TestAttestFailsIfServiceAccountNotAllowed() { @@ -230,7 +230,7 @@ func (s *AttestorSuite) TestAttestFailsIfCannotGetPod() { s.apiServerClient.SetTokenStatus(token, createTokenStatus(tokenData, true, defaultAudience)) s.requireAttestError(makePayload("FOO", token), codes.Internal, - "nodeattestor(k8s_psat): fail to get pod from k8s API server") + "nodeattestor(k8s_psat): failed to get pod from k8s API server") } func (s *AttestorSuite) TestAttestFailsIfCannotGetNode() { @@ -245,7 +245,7 @@ func (s *AttestorSuite) TestAttestFailsIfCannotGetNode() { s.apiServerClient.SetPod(createPod("NS1", "PODNAME", "PODUID", "NODENAME", "172.16.0.1")) s.requireAttestError(makePayload("FOO", token), codes.Internal, - "nodeattestor(k8s_psat): fail to get node from k8s API server") + "nodeattestor(k8s_psat): failed to get node from k8s API server") } func (s *AttestorSuite) TestAttestFailsIfNodeUIDIsEmpty() {