From e9c941a90aa3058b55f56c4ce38d2a6787526681 Mon Sep 17 00:00:00 2001 From: NopeCharon <115794254+NopeCharon@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:44:09 +0530 Subject: [PATCH] fix: request the audience of projected service account tokens remoteExecutionHandleProjectedSource built the TokenRequest for a serviceAccountToken projected source without its audience, so the offloaded container always got a token for the API server's default audiences. Pods that authenticate to an external service with their service account (Vault's Kubernetes auth, cloud workload identity, any OIDC relying party checking aud) had their token rejected on a virtual node. Like the kubelet, set Spec.Audiences to the source's audience when it is not empty. A source without an audience still gets a token for the API server's default audiences, as before. The new test captures the TokenRequest sent to the API server for a source with and without an audience, and checks the token returned for it is written at the source's path. Fixes #602 Signed-off-by: NopeCharon <115794254+NopeCharon@users.noreply.github.com> --- pkg/virtualkubelet/execute.go | 6 ++- pkg/virtualkubelet/execute_test.go | 70 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pkg/virtualkubelet/execute.go b/pkg/virtualkubelet/execute.go index 275f37f2..26db2468 100644 --- a/pkg/virtualkubelet/execute.go +++ b/pkg/virtualkubelet/execute.go @@ -806,11 +806,15 @@ func remoteExecutionHandleProjectedSource( } tokenRequest := &authenticationv1.TokenRequest{ Spec: authenticationv1.TokenRequestSpec{ - // No need to set audience field. If set with wrong value, it might break token validity! ExpirationSeconds: &expirationSeconds, BoundObjectRef: bountObjectRef, }, } + // Like the kubelet, request the audience the pod asks for. Without one, the token is + // issued for the API server's default audiences, which is what the Kubernetes API expects. + if source.ServiceAccountToken.Audience != "" { + tokenRequest.Spec.Audiences = []string{source.ServiceAccountToken.Audience} + } tokenRequestResult, err := p.clientSet.CoreV1().ServiceAccounts(pod.Namespace).CreateToken( ctx, pod.Spec.ServiceAccountName, tokenRequest, metav1.CreateOptions{}) diff --git a/pkg/virtualkubelet/execute_test.go b/pkg/virtualkubelet/execute_test.go index fd0ae978..a4764876 100644 --- a/pkg/virtualkubelet/execute_test.go +++ b/pkg/virtualkubelet/execute_test.go @@ -17,9 +17,12 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" oteltrace "go.opentelemetry.io/otel/trace" + authenticationv1 "k8s.io/api/authentication/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" + k8stesting "k8s.io/client-go/testing" ) const testNamespace = "test-ns" @@ -384,6 +387,73 @@ func TestGetSessionContextMessage(t *testing.T) { } } +func TestRemoteExecutionHandleProjectedSourceServiceAccountToken(t *testing.T) { + const testTokenPath = "token" + ctx := context.Background() + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespace, + UID: "token-pod-uid", + }, + Spec: v1.PodSpec{ + ServiceAccountName: "svc-account", + }, + } + + tests := []struct { + name string + audience string + expectedAudiences []string + }{ + { + name: "no audience requests the API server default audiences", + audience: "", + expectedAudiences: nil, + }, + { + name: "audience is requested for the token", + audience: "vault", + expectedAudiences: []string{"vault"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := fake.NewSimpleClientset() + var requested *authenticationv1.TokenRequest + fakeClient.PrependReactor("create", "serviceaccounts", func(action k8stesting.Action) (bool, runtime.Object, error) { + createAction, ok := action.(k8stesting.CreateAction) + if !ok || createAction.GetSubresource() != "token" { + return false, nil, nil + } + requested = createAction.GetObject().(*authenticationv1.TokenRequest).DeepCopy() + response := requested.DeepCopy() + response.Status.Token = "token-for-" + strings.Join(requested.Spec.Audiences, ",") + return true, response, nil + }) + + source := v1.VolumeProjection{ + ServiceAccountToken: &v1.ServiceAccountTokenProjection{ + Audience: tt.audience, + Path: testTokenPath, + }, + } + projectedVolume := &v1.ConfigMap{Data: map[string]string{}} + + err := remoteExecutionHandleProjectedSource(ctx, &Provider{clientSet: fakeClient}, pod, source, projectedVolume) + require.NoError(t, err) + + require.NotNil(t, requested) + assert.Equal(t, tt.expectedAudiences, requested.Spec.Audiences) + require.NotNil(t, requested.Spec.BoundObjectRef) + assert.Equal(t, "Pod", requested.Spec.BoundObjectRef.Kind) + assert.Equal(t, pod.UID, requested.Spec.BoundObjectRef.UID) + assert.Equal(t, "token-for-"+tt.audience, projectedVolume.Data[testTokenPath]) + }) + } +} + func TestRemoteExecutionHandleProjectedSourceConfigMap(t *testing.T) { ctx := context.Background() namespace := testNamespace