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 }