Skip to content

Commit 4cca432

Browse files
committed
Validate agent token for duplicate IP addresses
Problem: In some environments, Pods could share an IP address (for example when a Job completes and a new Pod grabs that IP). This would cause problems when the nginx data plane Pod would attempt to connect to the control plane. The control plane would try to validate the token provided by the nginx agent, by using the IP address in the request to lookup the associated Pod. If multiple Pods existed with that IP address, the control plane would error out. Solution: Fix the control plane logic to handle multiple Pods with the same IP address, and look through all of them to find a proper match for the token.
1 parent e138d64 commit 4cca432

File tree

2 files changed

+143
-54
lines changed

2 files changed

+143
-54
lines changed

internal/controller/nginx/agent/grpc/interceptor/interceptor.go

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
authv1 "k8s.io/api/authentication/v1"
1717
corev1 "k8s.io/api/core/v1"
1818
"k8s.io/apimachinery/pkg/fields"
19+
"k8s.io/apimachinery/pkg/labels"
1920
"sigs.k8s.io/controller-runtime/pkg/client"
2021

2122
grpcContext "github.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/agent/grpc/context"
@@ -160,34 +161,23 @@ func (c ContextSetter) validateToken(ctx context.Context, gi *grpcContext.GrpcIn
160161
var podList corev1.PodList
161162
opts := &client.ListOptions{
162163
FieldSelector: fields.SelectorFromSet(fields.Set{"status.podIP": gi.IPAddress}),
164+
Namespace: usernameItems[2],
165+
LabelSelector: labels.Set(map[string]string{
166+
controller.AppNameLabel: usernameItems[3],
167+
}).AsSelector(),
163168
}
164169

165170
if err := c.k8sClient.List(getCtx, &podList, opts); err != nil {
166171
return nil, status.Error(codes.Internal, fmt.Sprintf("error listing pods: %s", err.Error()))
167172
}
168173

169-
if len(podList.Items) != 1 {
170-
msg := fmt.Sprintf("expected one Pod to have IP address %s, found %d", gi.IPAddress, len(podList.Items))
171-
return nil, status.Error(codes.Internal, msg)
172-
}
173-
174-
podNS := podList.Items[0].GetNamespace()
175-
if podNS != usernameItems[2] {
176-
msg := fmt.Sprintf(
177-
"token user namespace %q does not match namespace of requesting pod %q", usernameItems[2], podNS,
178-
)
179-
return nil, status.Error(codes.Unauthenticated, msg)
180-
}
181-
182-
scName, ok := podList.Items[0].GetLabels()[controller.AppNameLabel]
183-
if !ok {
184-
msg := fmt.Sprintf("could not get app name from %q label; unable to authenticate token", controller.AppNameLabel)
185-
return nil, status.Error(codes.Unauthenticated, msg)
186-
}
187-
188-
if scName != usernameItems[3] {
174+
if len(podList.Items) == 0 {
189175
msg := fmt.Sprintf(
190-
"token user name %q does not match service account name of requesting pod %q", usernameItems[3], scName,
176+
"did not find any Pods with IP address %q, namespace %q, and %q label value %q",
177+
gi.IPAddress,
178+
usernameItems[2],
179+
controller.AppNameLabel,
180+
usernameItems[3],
191181
)
192182
return nil, status.Error(codes.Unauthenticated, msg)
193183
}

internal/controller/nginx/agent/grpc/interceptor/interceptor_test.go

Lines changed: 132 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import (
1717
corev1 "k8s.io/api/core/v1"
1818
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1919
"sigs.k8s.io/controller-runtime/pkg/client"
20+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2021

22+
grpcContext "github.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/agent/grpc/context"
2123
"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/controller"
2224
)
2325

@@ -204,38 +206,6 @@ func TestInterceptor(t *testing.T) {
204206
expErrCode: codes.Unauthenticated,
205207
expErrMsg: "must be of the format",
206208
},
207-
{
208-
name: "mismatched namespace in username",
209-
md: validMetadata,
210-
peer: validPeerData,
211-
username: "system:serviceaccount:invalid:gateway-nginx",
212-
appName: "gateway-nginx",
213-
podNamespace: "default",
214-
authenticated: true,
215-
expErrCode: codes.Unauthenticated,
216-
expErrMsg: "does not match namespace",
217-
},
218-
{
219-
name: "mismatched name in username",
220-
md: validMetadata,
221-
peer: validPeerData,
222-
username: "system:serviceaccount:default:invalid",
223-
appName: "gateway-nginx",
224-
podNamespace: "default",
225-
authenticated: true,
226-
expErrCode: codes.Unauthenticated,
227-
expErrMsg: "does not match service account name",
228-
},
229-
{
230-
name: "missing app name label",
231-
md: validMetadata,
232-
peer: validPeerData,
233-
username: "system:serviceaccount:default:gateway-nginx",
234-
podNamespace: "default",
235-
authenticated: true,
236-
expErrCode: codes.Unauthenticated,
237-
expErrMsg: "could not get app name",
238-
},
239209
}
240210

241211
streamHandler := func(_ any, _ grpc.ServerStream) error {
@@ -261,7 +231,7 @@ func TestInterceptor(t *testing.T) {
261231
}
262232
cs := NewContextSetter(mockK8sClient, "ngf-audience")
263233

264-
ctx := context.Background()
234+
ctx := t.Context()
265235
if test.md != nil {
266236
peerCtx := context.Background()
267237
if test.peer != nil {
@@ -290,3 +260,132 @@ func TestInterceptor(t *testing.T) {
290260
})
291261
}
292262
}
263+
264+
type patchClient struct {
265+
client.Client
266+
}
267+
268+
func (p *patchClient) Create(_ context.Context, obj client.Object, _ ...client.CreateOption) error {
269+
tr, ok := obj.(*authv1.TokenReview)
270+
if ok {
271+
tr.Status.Authenticated = true
272+
tr.Status.User.Username = "system:serviceaccount:default:gateway-nginx"
273+
}
274+
return nil
275+
}
276+
277+
func TestValidateToken_PodListOptions(t *testing.T) {
278+
t.Parallel()
279+
280+
testCases := []struct {
281+
pod *corev1.Pod
282+
gi *grpcContext.GrpcInfo
283+
name string
284+
shouldErr bool
285+
}{
286+
{
287+
name: "all match",
288+
pod: &corev1.Pod{
289+
ObjectMeta: metav1.ObjectMeta{
290+
Name: "nginx-pod",
291+
Namespace: "default",
292+
Labels: map[string]string{
293+
controller.AppNameLabel: "gateway-nginx",
294+
},
295+
},
296+
Status: corev1.PodStatus{PodIP: "1.2.3.4"},
297+
},
298+
gi: &grpcContext.GrpcInfo{Token: "dummy-token", IPAddress: "1.2.3.4"},
299+
shouldErr: false,
300+
},
301+
{
302+
name: "ip matches, namespace does not",
303+
pod: &corev1.Pod{
304+
ObjectMeta: metav1.ObjectMeta{
305+
Name: "nginx-pod",
306+
Namespace: "other-namespace",
307+
Labels: map[string]string{
308+
controller.AppNameLabel: "gateway-nginx",
309+
},
310+
},
311+
Status: corev1.PodStatus{PodIP: "1.2.3.4"},
312+
},
313+
gi: &grpcContext.GrpcInfo{Token: "dummy-token", IPAddress: "1.2.3.4"},
314+
shouldErr: true,
315+
},
316+
{
317+
name: "ip matches, label value does not match",
318+
pod: &corev1.Pod{
319+
ObjectMeta: metav1.ObjectMeta{
320+
Name: "nginx-pod",
321+
Namespace: "default",
322+
Labels: map[string]string{
323+
controller.AppNameLabel: "not-gateway-nginx",
324+
},
325+
},
326+
Status: corev1.PodStatus{PodIP: "1.2.3.4"},
327+
},
328+
gi: &grpcContext.GrpcInfo{Token: "dummy-token", IPAddress: "1.2.3.4"},
329+
shouldErr: true,
330+
},
331+
{
332+
name: "ip matches, label does not exist",
333+
pod: &corev1.Pod{
334+
ObjectMeta: metav1.ObjectMeta{
335+
Name: "nginx-pod",
336+
Namespace: "default",
337+
Labels: map[string]string{},
338+
},
339+
Status: corev1.PodStatus{PodIP: "1.2.3.4"},
340+
},
341+
gi: &grpcContext.GrpcInfo{Token: "dummy-token", IPAddress: "1.2.3.4"},
342+
shouldErr: true,
343+
},
344+
{
345+
name: "ip does not match",
346+
pod: &corev1.Pod{
347+
ObjectMeta: metav1.ObjectMeta{
348+
Name: "nginx-pod",
349+
Namespace: "default",
350+
Labels: map[string]string{
351+
controller.AppNameLabel: "gateway-nginx",
352+
},
353+
},
354+
Status: corev1.PodStatus{PodIP: "1.2.3.4"},
355+
},
356+
gi: &grpcContext.GrpcInfo{Token: "dummy-token", IPAddress: "9.9.9.9"},
357+
shouldErr: true,
358+
},
359+
}
360+
361+
for _, tc := range testCases {
362+
t.Run(tc.name, func(t *testing.T) {
363+
t.Parallel()
364+
g := NewWithT(t)
365+
366+
fakeClient := fake.NewClientBuilder().
367+
WithObjects(tc.pod).
368+
WithIndex(&corev1.Pod{}, "status.podIP", func(obj client.Object) []string {
369+
pod, ok := obj.(*corev1.Pod)
370+
g.Expect(ok).To(BeTrue())
371+
if pod.Status.PodIP != "" {
372+
return []string{pod.Status.PodIP}
373+
}
374+
return nil
375+
}).
376+
Build()
377+
378+
patchedClient := &patchClient{fakeClient}
379+
csPatched := NewContextSetter(patchedClient, "ngf-audience")
380+
381+
resultCtx, err := csPatched.validateToken(t.Context(), tc.gi)
382+
if tc.shouldErr {
383+
g.Expect(err).To(HaveOccurred())
384+
g.Expect(err.Error()).To(ContainSubstring("did not find any Pods"))
385+
} else {
386+
g.Expect(err).ToNot(HaveOccurred())
387+
g.Expect(resultCtx).ToNot(BeNil())
388+
}
389+
})
390+
}
391+
}

0 commit comments

Comments
 (0)