Skip to content

Commit 654a617

Browse files
committed
Verify pod name for authz
This reverts commit b0748a0.
1 parent 6f0a844 commit 654a617

File tree

3 files changed

+78
-26
lines changed

3 files changed

+78
-26
lines changed

internal/xds/cache/snapshotcache.go

+14
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type SnapshotCacheWithCallbacks interface {
4747
cachev3.SnapshotCache
4848
serverv3.Callbacks
4949
GenerateNewSnapshot(string, types.XdsResources) error
50+
SnapshotHasIrKey(string) bool
5051
GetIrKeys() []string
5152
}
5253

@@ -365,6 +366,19 @@ func (s *snapshotCache) OnFetchRequest(_ context.Context, _ *discoveryv3.Discove
365366
func (s *snapshotCache) OnFetchResponse(_ *discoveryv3.DiscoveryRequest, _ *discoveryv3.DiscoveryResponse) {
366367
}
367368

369+
func (s *snapshotCache) SnapshotHasIrKey(irKey string) bool {
370+
s.mu.Lock()
371+
defer s.mu.Unlock()
372+
373+
for key, snapshot := range s.lastSnapshot {
374+
if snapshot != nil && key == irKey {
375+
return true
376+
}
377+
}
378+
379+
return false
380+
}
381+
368382
func (s *snapshotCache) GetIrKeys() []string {
369383
s.mu.Lock()
370384
defer s.mu.Unlock()

internal/xds/server/kubejwt/jwtinterceptor.go

+46-21
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
package kubejwt
77

88
import (
9+
"context"
910
"fmt"
1011
"strings"
1112

13+
discoveryv3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
1214
"google.golang.org/grpc"
1315
"google.golang.org/grpc/metadata"
1416
"k8s.io/client-go/kubernetes"
@@ -34,33 +36,56 @@ func NewJWTAuthInterceptor(clientset *kubernetes.Clientset, issuer, audience str
3436
}
3537
}
3638

37-
// Stream intercepts streaming gRPC calls for authentication.
38-
func (i *JWTAuthInterceptor) Stream() grpc.StreamServerInterceptor {
39-
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
40-
if err := i.authorize(ss); err != nil {
41-
return err
42-
}
43-
return handler(srv, ss)
44-
}
39+
type wrappedStream struct {
40+
grpc.ServerStream
41+
ctx context.Context
42+
interceptor *JWTAuthInterceptor
43+
validated bool
4544
}
4645

47-
// authorize validates the Kubernetes Service Account JWT token from the metadata.
48-
func (i *JWTAuthInterceptor) authorize(ss grpc.ServerStream) error {
49-
md, ok := metadata.FromIncomingContext(ss.Context())
50-
if !ok {
51-
return fmt.Errorf("missing metadata")
46+
func (w *wrappedStream) RecvMsg(m any) error {
47+
err := w.ServerStream.RecvMsg(m)
48+
if err != nil {
49+
return err
5250
}
5351

54-
authHeader, exists := md["authorization"]
55-
if !exists || len(authHeader) == 0 {
56-
return fmt.Errorf("missing authorization token in metadata: %s", md)
57-
}
58-
tokenStr := strings.TrimPrefix(authHeader[0], "Bearer ")
52+
if !w.validated {
53+
if req, ok := m.(*discoveryv3.DeltaDiscoveryRequest); ok {
54+
if req.Node == nil || req.Node.Id == "" {
55+
return fmt.Errorf("missing node ID in request")
56+
}
57+
nodeID := req.Node.Id
5958

60-
err := i.validateKubeJWT(ss.Context(), tokenStr)
61-
if err != nil {
62-
return fmt.Errorf("failed to validate token: %w", err)
59+
md, ok := metadata.FromIncomingContext(w.ctx)
60+
if !ok {
61+
return fmt.Errorf("missing metadata")
62+
}
63+
64+
authHeader := md.Get("authorization")
65+
if len(authHeader) == 0 {
66+
return fmt.Errorf("missing authorization token in metadata: %s", md)
67+
}
68+
token := strings.TrimPrefix(authHeader[0], "Bearer ")
69+
70+
if err := w.interceptor.validateKubeJWT(w.ctx, token, nodeID); err != nil {
71+
return fmt.Errorf("failed to validate token: %w", err)
72+
}
73+
74+
w.validated = true
75+
}
6376
}
6477

6578
return nil
6679
}
80+
81+
func newWrappedStream(s grpc.ServerStream, ctx context.Context, interceptor *JWTAuthInterceptor) grpc.ServerStream {
82+
return &wrappedStream{s, ctx, interceptor, false}
83+
}
84+
85+
// Stream intercepts streaming gRPC calls for authorization.
86+
func (i *JWTAuthInterceptor) Stream() grpc.StreamServerInterceptor {
87+
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
88+
wrapped := newWrappedStream(ss, ss.Context(), i)
89+
return handler(srv, wrapped)
90+
}
91+
}

internal/xds/server/kubejwt/tokenreview.go

+18-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
authenticationv1 "k8s.io/api/authentication/v1"
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apiserver/pkg/authentication/serviceaccount"
1617
"k8s.io/client-go/kubernetes"
1718
"k8s.io/client-go/rest"
1819

@@ -35,7 +36,7 @@ func GetKubernetesClient() (*kubernetes.Clientset, error) {
3536
return clientset, nil
3637
}
3738

38-
func (i *JWTAuthInterceptor) validateKubeJWT(ctx context.Context, token string) error {
39+
func (i *JWTAuthInterceptor) validateKubeJWT(ctx context.Context, token, nodeID string) error {
3940
tokenReview := &authenticationv1.TokenReview{
4041
Spec: authenticationv1.TokenReviewSpec{
4142
Token: token,
@@ -56,8 +57,21 @@ func (i *JWTAuthInterceptor) validateKubeJWT(ctx context.Context, token string)
5657
return fmt.Errorf("token is not authenticated")
5758
}
5859

59-
// Check if the service account name in the JWT token exists in the cache to verify that the token
60-
// is valid for an Envoy proxy managed by Envoy Gateway.
60+
// Check if the node ID in the request matches the pod name in the token review response.
61+
// This is used to prevent a client from accessing the xDS resource of another one.
62+
if tokenReview.Status.User.Extra != nil {
63+
podName := tokenReview.Status.User.Extra[serviceaccount.PodNameKey]
64+
if podName[0] == "" {
65+
return fmt.Errorf("pod name not found in token review response")
66+
}
67+
68+
if podName[0] != nodeID {
69+
return fmt.Errorf("pod name mismatch: expected %s, got %s", nodeID, podName[0])
70+
}
71+
}
72+
73+
// Check if the service account name in the JWT token exists in the cache.
74+
// This is used to verify that the token belongs to a valid Enovy managed by Envoy Gateway.
6175
// example: "system:serviceaccount:default:envoy-default-eg-e41e7b31"
6276
parts := strings.Split(tokenReview.Status.User.Username, ":")
6377
if len(parts) != 4 {
@@ -71,8 +85,7 @@ func (i *JWTAuthInterceptor) validateKubeJWT(ctx context.Context, token string)
7185
return nil
7286
}
7387
}
74-
75-
return fmt.Errorf("envoy service account %s not found in the cache", sa)
88+
return fmt.Errorf("Envoy service account %s not found in the cache", sa)
7689
}
7790

7891
// this is the same logic used in infra pkg func ExpectedResourceHashedName to generate the resource name.

0 commit comments

Comments
 (0)