istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/server/ca/node_auth_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ca
    16  
    17  import (
    18  	"context"
    19  	"strings"
    20  	"testing"
    21  
    22  	"google.golang.org/grpc/metadata"
    23  	v1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	"k8s.io/apimachinery/pkg/types"
    27  
    28  	"istio.io/istio/pkg/cluster"
    29  	"istio.io/istio/pkg/kube"
    30  	"istio.io/istio/pkg/kube/multicluster"
    31  	"istio.io/istio/pkg/security"
    32  	"istio.io/istio/pkg/spiffe"
    33  	"istio.io/istio/pkg/test"
    34  )
    35  
    36  type pod struct {
    37  	name, namespace, account, node, uid string
    38  }
    39  
    40  func (p pod) Identity() string {
    41  	return spiffe.Identity{
    42  		TrustDomain:    "cluster.local",
    43  		Namespace:      p.namespace,
    44  		ServiceAccount: p.account,
    45  	}.String()
    46  }
    47  
    48  func TestSingleClusterNodeAuthorization(t *testing.T) {
    49  	allowZtunnel := map[types.NamespacedName]struct{}{
    50  		{Name: "ztunnel", Namespace: "istio-system"}: {},
    51  	}
    52  	ztunnelCaller := security.KubernetesInfo{
    53  		PodName:           "ztunnel-a",
    54  		PodNamespace:      "istio-system",
    55  		PodUID:            "12345",
    56  		PodServiceAccount: "ztunnel",
    57  	}
    58  	ztunnelPod := pod{
    59  		name:      ztunnelCaller.PodName,
    60  		namespace: ztunnelCaller.PodNamespace,
    61  		account:   ztunnelCaller.PodServiceAccount,
    62  		uid:       ztunnelCaller.PodUID,
    63  		node:      "zt-node",
    64  	}
    65  	podSameNode := pod{
    66  		name:      "pod-a",
    67  		namespace: "ns-a",
    68  		account:   "sa-a",
    69  		uid:       "1",
    70  		node:      "zt-node",
    71  	}
    72  	podOtherNode := pod{
    73  		name:      "pod-b",
    74  		namespace: podSameNode.namespace,
    75  		account:   podSameNode.account,
    76  		uid:       "2",
    77  		node:      "other-node",
    78  	}
    79  	cases := []struct {
    80  		name                    string
    81  		pods                    []pod
    82  		caller                  security.KubernetesInfo
    83  		requestedIdentityString string
    84  		trustedAccounts         map[types.NamespacedName]struct{}
    85  		wantErr                 string
    86  	}{
    87  		{
    88  			name:    "empty allowed identities",
    89  			wantErr: "not allowed to impersonate",
    90  		},
    91  		{
    92  			name:                    "allowed identities, but not on node",
    93  			caller:                  ztunnelCaller,
    94  			trustedAccounts:         allowZtunnel,
    95  			requestedIdentityString: podSameNode.Identity(),
    96  			pods:                    []pod{ztunnelPod},
    97  			wantErr:                 "no instances",
    98  		},
    99  		{
   100  			name:                    "allowed identities, on node",
   101  			caller:                  ztunnelCaller,
   102  			trustedAccounts:         allowZtunnel,
   103  			requestedIdentityString: podSameNode.Identity(),
   104  			pods:                    []pod{ztunnelPod, podSameNode},
   105  			wantErr:                 "",
   106  		},
   107  		{
   108  			name:                    "allowed identities, off node",
   109  			caller:                  ztunnelCaller,
   110  			trustedAccounts:         allowZtunnel,
   111  			requestedIdentityString: podSameNode.Identity(),
   112  			pods:                    []pod{ztunnelPod, podOtherNode},
   113  			wantErr:                 "no instances",
   114  		},
   115  		{
   116  			name:                    "allowed identities, on and off node",
   117  			caller:                  ztunnelCaller,
   118  			trustedAccounts:         allowZtunnel,
   119  			requestedIdentityString: podSameNode.Identity(),
   120  			pods:                    []pod{ztunnelPod, podSameNode, podOtherNode},
   121  			wantErr:                 "",
   122  		},
   123  		{
   124  			name:                    "invalid requested",
   125  			caller:                  ztunnelCaller,
   126  			trustedAccounts:         allowZtunnel,
   127  			requestedIdentityString: "not-spiffe-idenditity",
   128  			pods:                    []pod{ztunnelPod},
   129  			wantErr:                 "failed to validate impersonated identity",
   130  		},
   131  		{
   132  			name:                    "unknown caller",
   133  			caller:                  ztunnelCaller,
   134  			trustedAccounts:         allowZtunnel,
   135  			requestedIdentityString: podSameNode.Identity(),
   136  			pods:                    []pod{podSameNode},
   137  			wantErr:                 "pod istio-system/ztunnel-a not found",
   138  		},
   139  		{
   140  			name: "bad UID",
   141  			caller: func(k security.KubernetesInfo) security.KubernetesInfo {
   142  				k.PodUID = "bogus"
   143  				return k
   144  			}(ztunnelCaller),
   145  			trustedAccounts:         allowZtunnel,
   146  			requestedIdentityString: podSameNode.Identity(),
   147  			pods:                    []pod{ztunnelPod},
   148  			wantErr:                 "pod found, but UID does not match",
   149  		},
   150  		{
   151  			name:                    "bad account",
   152  			caller:                  ztunnelCaller,
   153  			trustedAccounts:         allowZtunnel,
   154  			requestedIdentityString: podSameNode.Identity(),
   155  			pods: []pod{func(p pod) pod {
   156  				p.account = "bogus"
   157  				return p
   158  			}(ztunnelPod)},
   159  			wantErr: "pod found, but ServiceAccount does not match",
   160  		},
   161  	}
   162  	for _, tt := range cases {
   163  		t.Run(tt.name, func(t *testing.T) {
   164  			var pods []runtime.Object
   165  			for _, p := range tt.pods {
   166  				pods = append(pods, &v1.Pod{
   167  					ObjectMeta: metav1.ObjectMeta{
   168  						Name:      p.name,
   169  						Namespace: p.namespace,
   170  						UID:       types.UID(p.uid),
   171  					},
   172  					Spec: v1.PodSpec{
   173  						ServiceAccountName: p.account,
   174  						NodeName:           p.node,
   175  					},
   176  				})
   177  			}
   178  			c := kube.NewFakeClient(pods...)
   179  			na := NewClusterNodeAuthorizer(c, tt.trustedAccounts)
   180  			c.RunAndWait(test.NewStop(t))
   181  			kube.WaitForCacheSync("test", test.NewStop(t), na.pods.HasSynced)
   182  
   183  			err := na.authenticateImpersonation(tt.caller, tt.requestedIdentityString)
   184  			if tt.wantErr == "" && err != nil {
   185  				t.Fatalf("wanted no error, got %v", err)
   186  			}
   187  			if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
   188  				t.Fatalf("expected error %q, got %q", tt.wantErr, err)
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  func toPod(p pod, isZtunnel bool) *v1.Pod {
   195  	po := &v1.Pod{
   196  		ObjectMeta: metav1.ObjectMeta{
   197  			Name:      p.name,
   198  			Namespace: p.namespace,
   199  			UID:       types.UID(p.uid),
   200  		},
   201  		Spec: v1.PodSpec{
   202  			ServiceAccountName: p.account,
   203  			NodeName:           p.node,
   204  		},
   205  	}
   206  	if isZtunnel {
   207  		po.Labels = map[string]string{
   208  			"app": "ztunnel",
   209  		}
   210  	}
   211  	return po
   212  }
   213  
   214  func TestMultiClusterNodeAuthorization(t *testing.T) {
   215  	allowZtunnel := map[types.NamespacedName]struct{}{
   216  		{Name: "ztunnel", Namespace: "istio-system"}: {},
   217  	}
   218  	ztunnelCallerPrimary := security.KubernetesInfo{
   219  		PodName:           "ztunnel-a",
   220  		PodNamespace:      "istio-system",
   221  		PodUID:            "12345",
   222  		PodServiceAccount: "ztunnel",
   223  	}
   224  	ztunnelPodPrimary := pod{
   225  		name:      ztunnelCallerPrimary.PodName,
   226  		namespace: ztunnelCallerPrimary.PodNamespace,
   227  		account:   ztunnelCallerPrimary.PodServiceAccount,
   228  		uid:       ztunnelCallerPrimary.PodUID,
   229  		node:      "zt-node-primary",
   230  	}
   231  	ztunnelCallerRemote := security.KubernetesInfo{
   232  		PodName:           "ztunnel-b",
   233  		PodNamespace:      "istio-system",
   234  		PodUID:            "12346",
   235  		PodServiceAccount: "ztunnel",
   236  	}
   237  	ztunnelPodRemote := pod{
   238  		name:      ztunnelCallerRemote.PodName,
   239  		namespace: ztunnelCallerRemote.PodNamespace,
   240  		account:   ztunnelCallerRemote.PodServiceAccount,
   241  		uid:       ztunnelCallerRemote.PodUID,
   242  		node:      "zt-node-remote",
   243  	}
   244  	ztunnelCallerRemote2 := security.KubernetesInfo{
   245  		PodName:           "ztunnel-c",
   246  		PodNamespace:      "istio-system",
   247  		PodUID:            "12347",
   248  		PodServiceAccount: "ztunnel",
   249  	}
   250  	ztunnelPodRemote2 := pod{
   251  		name:      ztunnelCallerRemote2.PodName,
   252  		namespace: ztunnelCallerRemote2.PodNamespace,
   253  		account:   ztunnelCallerRemote2.PodServiceAccount,
   254  		uid:       ztunnelCallerRemote2.PodUID,
   255  		node:      "zt-node-remote",
   256  	}
   257  	podSameNodePrimary := pod{
   258  		name:      "pod-a",
   259  		namespace: "ns-a",
   260  		account:   "sa-a",
   261  		uid:       "1",
   262  		node:      "zt-node-primary",
   263  	}
   264  	podSameNodeRemote := pod{
   265  		name:      "pod-b",
   266  		namespace: "ns-b",
   267  		account:   "sa-b",
   268  		uid:       "2",
   269  		node:      "zt-node-remote",
   270  	}
   271  	primaryClusterPods := []runtime.Object{
   272  		toPod(ztunnelPodPrimary, true),
   273  		toPod(podSameNodePrimary, false),
   274  	}
   275  	remoteClusterPods := []runtime.Object{
   276  		toPod(ztunnelPodRemote, true),
   277  		toPod(podSameNodeRemote, false),
   278  	}
   279  	remoteCluster2Pods := []runtime.Object{
   280  		toPod(ztunnelPodRemote2, true),
   281  	}
   282  
   283  	primaryClient := kube.NewFakeClient(primaryClusterPods...)
   284  
   285  	remoteClient := kube.NewFakeClient(remoteClusterPods...)
   286  
   287  	remote2Client := kube.NewFakeClient(remoteCluster2Pods...)
   288  
   289  	mc := multicluster.NewFakeController()
   290  	mNa := NewMulticlusterNodeAuthenticator(allowZtunnel, mc)
   291  	stop := test.NewStop(t)
   292  	mc.Add("primary", primaryClient, stop)
   293  	mc.Add("remote", remoteClient, stop)
   294  	mc.Add("remote2", remote2Client, stop)
   295  	primaryClient.RunAndWait(stop)
   296  	remoteClient.RunAndWait(stop)
   297  	remote2Client.RunAndWait(stop)
   298  	mc.Delete("remote2")
   299  
   300  	for _, c := range mNa.component.All() {
   301  		kube.WaitForCacheSync("test", stop, c.pods.HasSynced)
   302  	}
   303  	cases := []struct {
   304  		name                    string
   305  		callerClusterID         cluster.ID
   306  		caller                  security.KubernetesInfo
   307  		requestedIdentityString string
   308  		wantErr                 string
   309  	}{
   310  		{
   311  			name:                    "allowed identities, on node of primary cluster",
   312  			callerClusterID:         cluster.ID("primary"),
   313  			caller:                  ztunnelCallerPrimary,
   314  			requestedIdentityString: podSameNodePrimary.Identity(),
   315  			wantErr:                 "",
   316  		},
   317  		{
   318  			name:                    "allowed identities, on node of remote cluster",
   319  			callerClusterID:         cluster.ID("remote"),
   320  			caller:                  ztunnelCallerRemote,
   321  			requestedIdentityString: podSameNodeRemote.Identity(),
   322  			wantErr:                 "",
   323  		},
   324  		{
   325  			name:            "ztunnel caller from removed remote cluster",
   326  			callerClusterID: cluster.ID("remote2"),
   327  			caller:          ztunnelCallerRemote2,
   328  			wantErr:         "no node authorizer",
   329  		},
   330  		{
   331  			name:                    "allowed identities in remote cluster, but ztunnel caller from primary cluster",
   332  			callerClusterID:         cluster.ID("primary"),
   333  			caller:                  ztunnelCallerPrimary,
   334  			requestedIdentityString: podSameNodeRemote.Identity(),
   335  			wantErr:                 "no instance",
   336  		},
   337  	}
   338  
   339  	for _, tt := range cases {
   340  		t.Run(tt.name, func(t *testing.T) {
   341  			ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
   342  				"clusterid": []string{string(tt.callerClusterID)},
   343  			})
   344  			err := mNa.authenticateImpersonation(ctx, tt.caller, tt.requestedIdentityString)
   345  			if tt.wantErr == "" && err != nil {
   346  				t.Fatalf("wanted no error, got %v", err)
   347  			}
   348  			if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
   349  				t.Fatalf("expected error %q, got %q", tt.wantErr, err)
   350  			}
   351  		})
   352  	}
   353  }