istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/server/ca/server_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 "crypto/tls" 20 "crypto/x509" 21 "crypto/x509/pkix" 22 "fmt" 23 "net" 24 "strings" 25 "testing" 26 "time" 27 28 "google.golang.org/grpc/codes" 29 "google.golang.org/grpc/credentials" 30 "google.golang.org/grpc/metadata" 31 "google.golang.org/grpc/peer" 32 "google.golang.org/grpc/status" 33 "google.golang.org/protobuf/types/known/structpb" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/types" 36 37 pb "istio.io/api/security/v1alpha1" 38 "istio.io/istio/pilot/pkg/features" 39 "istio.io/istio/pkg/cluster" 40 "istio.io/istio/pkg/kube" 41 "istio.io/istio/pkg/kube/multicluster" 42 "istio.io/istio/pkg/security" 43 "istio.io/istio/pkg/test" 44 "istio.io/istio/pkg/util/sets" 45 mockca "istio.io/istio/security/pkg/pki/ca/mock" 46 caerror "istio.io/istio/security/pkg/pki/error" 47 "istio.io/istio/security/pkg/pki/util" 48 "istio.io/istio/security/pkg/server/ca/authenticate" 49 ) 50 51 type mockAuthenticator struct { 52 authSource security.AuthSource 53 identities []string 54 kubernetesInfo security.KubernetesInfo 55 errMsg string 56 } 57 58 func (authn *mockAuthenticator) AuthenticatorType() string { 59 return "mockAuthenticator" 60 } 61 62 func (authn *mockAuthenticator) Authenticate(_ security.AuthContext) (*security.Caller, error) { 63 if len(authn.errMsg) > 0 { 64 return nil, fmt.Errorf("%v", authn.errMsg) 65 } 66 67 return &security.Caller{ 68 AuthSource: authn.authSource, 69 Identities: authn.identities, 70 KubernetesInfo: authn.kubernetesInfo, 71 }, nil 72 } 73 74 type mockAuthInfo struct { 75 authType string 76 } 77 78 func (ai mockAuthInfo) AuthType() string { 79 return ai.authType 80 } 81 82 /* 83 This is a testing to send a request to the server using 84 the client cert authenticator instead of mock authenticator 85 */ 86 func TestCreateCertificateE2EUsingClientCertAuthenticator(t *testing.T) { 87 callerID := "test.identity" 88 ids := []util.Identity{ 89 {Type: util.TypeURI, Value: []byte(callerID)}, 90 } 91 sanExt, err := util.BuildSANExtension(ids) 92 if err != nil { 93 t.Error(err) 94 } 95 auth := &authenticate.ClientCertAuthenticator{} 96 97 server := &Server{ 98 ca: &mockca.FakeCA{ 99 SignedCert: []byte("cert"), 100 KeyCertBundle: util.NewKeyCertBundleFromPem(nil, nil, []byte("cert_chain"), []byte("root_cert")), 101 }, 102 Authenticators: []security.Authenticator{auth}, 103 monitoring: newMonitoringMetrics(), 104 } 105 mockCertChain := []string{"cert", "cert_chain", "root_cert"} 106 mockIPAddr := &net.IPAddr{IP: net.IPv4(192, 168, 1, 1)} 107 testCerts := map[string]struct { 108 certChain [][]*x509.Certificate 109 caller *security.Caller 110 fakeAuthInfo *mockAuthInfo 111 code codes.Code 112 ipAddr *net.IPAddr 113 }{ 114 // no client certificate is presented 115 "No client certificate": { 116 certChain: nil, 117 caller: nil, 118 ipAddr: mockIPAddr, 119 code: codes.Unauthenticated, 120 }, 121 // "unsupported auth type: not-tls" 122 "Unsupported auth type": { 123 certChain: nil, 124 caller: nil, 125 fakeAuthInfo: &mockAuthInfo{"not-tls"}, 126 ipAddr: mockIPAddr, 127 code: codes.Unauthenticated, 128 }, 129 // no cert chain presented 130 "Empty cert chain": { 131 certChain: [][]*x509.Certificate{}, 132 caller: nil, 133 ipAddr: mockIPAddr, 134 code: codes.Unauthenticated, 135 }, 136 // certificate misses the SAN field 137 "Certificate has no SAN": { 138 certChain: [][]*x509.Certificate{ 139 { 140 { 141 Version: 1, 142 }, 143 }, 144 }, 145 ipAddr: mockIPAddr, 146 code: codes.Unauthenticated, 147 }, 148 // successful testcase with valid client certificate 149 "With client certificate": { 150 certChain: [][]*x509.Certificate{ 151 { 152 { 153 Extensions: []pkix.Extension{*sanExt}, 154 }, 155 }, 156 }, 157 caller: &security.Caller{Identities: []string{callerID}}, 158 ipAddr: mockIPAddr, 159 code: codes.OK, 160 }, 161 } 162 163 for id, c := range testCerts { 164 request := &pb.IstioCertificateRequest{Csr: "dumb CSR"} 165 ctx := context.Background() 166 if c.certChain != nil { 167 tlsInfo := credentials.TLSInfo{ 168 State: tls.ConnectionState{VerifiedChains: c.certChain}, 169 } 170 p := &peer.Peer{Addr: c.ipAddr, AuthInfo: tlsInfo} 171 ctx = peer.NewContext(ctx, p) 172 } 173 if c.fakeAuthInfo != nil { 174 ctx = peer.NewContext(ctx, &peer.Peer{Addr: c.ipAddr, AuthInfo: c.fakeAuthInfo}) 175 } 176 response, err := server.CreateCertificate(ctx, request) 177 178 s, _ := status.FromError(err) 179 code := s.Code() 180 if code != c.code { 181 t.Errorf("Case %s: expecting code to be (%d) but got (%d): %s", id, c.code, code, s.Message()) 182 } else if c.code == codes.OK { 183 if len(response.CertChain) != len(mockCertChain) { 184 t.Errorf("Case %s: expecting cert chain length to be (%d) but got (%d)", 185 id, len(mockCertChain), len(response.CertChain)) 186 } 187 for i, v := range response.CertChain { 188 if v != mockCertChain[i] { 189 t.Errorf("Case %s: expecting cert to be (%s) but got (%s) at position [%d] of cert chain.", 190 id, mockCertChain, v, i) 191 } 192 } 193 } 194 } 195 } 196 197 func TestCreateCertificate(t *testing.T) { 198 testCases := map[string]struct { 199 authenticators []security.Authenticator 200 ca CertificateAuthority 201 certChain []string 202 code codes.Code 203 }{ 204 "No authenticator": { 205 authenticators: nil, 206 code: codes.Unauthenticated, 207 ca: &mockca.FakeCA{}, 208 }, 209 "Unauthenticated request": { 210 authenticators: []security.Authenticator{&mockAuthenticator{ 211 errMsg: "Not authorized", 212 }}, 213 code: codes.Unauthenticated, 214 ca: &mockca.FakeCA{}, 215 }, 216 "CA not ready": { 217 authenticators: []security.Authenticator{&mockAuthenticator{identities: []string{"test-identity"}}}, 218 ca: &mockca.FakeCA{SignErr: caerror.NewError(caerror.CANotReady, fmt.Errorf("cannot sign"))}, 219 code: codes.Internal, 220 }, 221 "Invalid CSR": { 222 authenticators: []security.Authenticator{&mockAuthenticator{identities: []string{"test-identity"}}}, 223 ca: &mockca.FakeCA{SignErr: caerror.NewError(caerror.CSRError, fmt.Errorf("cannot sign"))}, 224 code: codes.InvalidArgument, 225 }, 226 "Invalid TTL": { 227 authenticators: []security.Authenticator{&mockAuthenticator{identities: []string{"test-identity"}}}, 228 ca: &mockca.FakeCA{SignErr: caerror.NewError(caerror.TTLError, fmt.Errorf("cannot sign"))}, 229 code: codes.InvalidArgument, 230 }, 231 "Failed to sign": { 232 authenticators: []security.Authenticator{&mockAuthenticator{identities: []string{"test-identity"}}}, 233 ca: &mockca.FakeCA{SignErr: caerror.NewError(caerror.CertGenError, fmt.Errorf("cannot sign"))}, 234 code: codes.Internal, 235 }, 236 "Successful signing": { 237 authenticators: []security.Authenticator{&mockAuthenticator{identities: []string{"test-identity"}}}, 238 ca: &mockca.FakeCA{ 239 SignedCert: []byte("cert"), 240 KeyCertBundle: util.NewKeyCertBundleFromPem(nil, nil, []byte("cert_chain"), []byte("root_cert")), 241 }, 242 certChain: []string{"cert", "cert_chain", "root_cert"}, 243 code: codes.OK, 244 }, 245 } 246 247 p := &peer.Peer{Addr: &net.IPAddr{IP: net.IPv4(192, 168, 1, 1)}, AuthInfo: credentials.TLSInfo{}} 248 ctx := peer.NewContext(context.Background(), p) 249 for id, c := range testCases { 250 server := &Server{ 251 ca: c.ca, 252 Authenticators: c.authenticators, 253 monitoring: newMonitoringMetrics(), 254 } 255 request := &pb.IstioCertificateRequest{Csr: "dumb CSR"} 256 257 response, err := server.CreateCertificate(ctx, request) 258 s, _ := status.FromError(err) 259 code := s.Code() 260 if c.code != code { 261 t.Errorf("Case %s: expecting code to be (%d) but got (%d): %s", id, c.code, code, s.Message()) 262 } else if c.code == codes.OK { 263 if len(response.CertChain) != len(c.certChain) { 264 t.Errorf("Case %s: expecting cert chain length to be (%d) but got (%d)", 265 id, len(c.certChain), len(response.CertChain)) 266 } 267 for i, v := range response.CertChain { 268 if v != c.certChain[i] { 269 t.Errorf("Case %s: expecting cert to be (%s) but got (%s) at position [%d] of cert chain.", 270 id, c.certChain, v, i) 271 } 272 } 273 274 } 275 } 276 } 277 278 func TestCreateCertificateE2EWithImpersonateIdentity(t *testing.T) { 279 allowZtunnel := sets.Set[types.NamespacedName]{ 280 {Name: "ztunnel", Namespace: "istio-system"}: {}, 281 } 282 ztunnelCaller := security.KubernetesInfo{ 283 PodName: "ztunnel-a", 284 PodNamespace: "istio-system", 285 PodUID: "12345", 286 PodServiceAccount: "ztunnel", 287 } 288 ztunnelPod := pod{ 289 name: ztunnelCaller.PodName, 290 namespace: ztunnelCaller.PodNamespace, 291 account: ztunnelCaller.PodServiceAccount, 292 uid: ztunnelCaller.PodUID, 293 node: "zt-node", 294 } 295 podSameNode := pod{ 296 name: "pod-a", 297 namespace: "ns-a", 298 account: "sa-a", 299 uid: "1", 300 node: "zt-node", 301 } 302 podOtherNode := pod{ 303 name: "pod-b", 304 namespace: podSameNode.namespace, 305 account: podSameNode.account, 306 uid: "2", 307 node: "other-node", 308 } 309 310 ztunnelCallerRemote := security.KubernetesInfo{ 311 PodName: "ztunnel-b", 312 PodNamespace: "istio-system", 313 PodUID: "12346", 314 PodServiceAccount: "ztunnel", 315 } 316 ztunnelPodRemote := pod{ 317 name: ztunnelCallerRemote.PodName, 318 namespace: ztunnelCallerRemote.PodNamespace, 319 account: ztunnelCallerRemote.PodServiceAccount, 320 uid: ztunnelCallerRemote.PodUID, 321 node: "zt-node-remote", 322 } 323 podSameNodeRemote := pod{ 324 name: "pod-c", 325 namespace: podSameNode.namespace, 326 account: podSameNode.account, 327 uid: "3", 328 node: "zt-node-remote", 329 } 330 331 testCases := []struct { 332 name string 333 authenticators []security.Authenticator 334 ca CertificateAuthority 335 certChain []string 336 pods []pod 337 impersonatePod pod 338 callerClusterID cluster.ID 339 trustedNodeAccounts sets.Set[types.NamespacedName] 340 isMultiCluster bool 341 remoteClusterPods []pod 342 code codes.Code 343 }{ 344 { 345 name: "No node authorizer", 346 authenticators: []security.Authenticator{&mockAuthenticator{ 347 identities: []string{"test-identity"}, 348 kubernetesInfo: ztunnelCaller, 349 }}, 350 ca: &mockca.FakeCA{ 351 SignedCert: []byte("cert"), 352 KeyCertBundle: util.NewKeyCertBundleFromPem(nil, nil, []byte("cert_chain"), []byte("root_cert")), 353 }, 354 certChain: []string{"cert", "cert_chain", "root_cert"}, 355 trustedNodeAccounts: sets.Set[types.NamespacedName]{}, 356 code: codes.Unauthenticated, 357 }, 358 { 359 name: "Pod not passing node authorization", 360 authenticators: []security.Authenticator{&mockAuthenticator{ 361 identities: []string{"test-identity"}, 362 kubernetesInfo: ztunnelCaller, 363 }}, 364 ca: &mockca.FakeCA{ 365 SignedCert: []byte("cert"), 366 KeyCertBundle: util.NewKeyCertBundleFromPem(nil, nil, []byte("cert_chain"), []byte("root_cert")), 367 }, 368 certChain: []string{"cert", "cert_chain", "root_cert"}, 369 pods: []pod{ztunnelPod, podOtherNode}, 370 impersonatePod: podOtherNode, 371 callerClusterID: cluster.ID("fake"), 372 trustedNodeAccounts: allowZtunnel, 373 code: codes.Unauthenticated, 374 }, 375 { 376 name: "Successful signing with impersonate identity", 377 authenticators: []security.Authenticator{&mockAuthenticator{ 378 identities: []string{"test-identity"}, 379 kubernetesInfo: ztunnelCaller, 380 }}, 381 ca: &mockca.FakeCA{ 382 SignedCert: []byte("cert"), 383 KeyCertBundle: util.NewKeyCertBundleFromPem(nil, nil, []byte("cert_chain"), []byte("root_cert")), 384 }, 385 certChain: []string{"cert", "cert_chain", "root_cert"}, 386 pods: []pod{ztunnelPod, podSameNode}, 387 impersonatePod: podSameNode, 388 callerClusterID: cluster.ID("fake"), 389 trustedNodeAccounts: allowZtunnel, 390 code: codes.OK, 391 }, 392 { 393 name: "Pod not passing node authorization because of ztunnel from other clusters", 394 authenticators: []security.Authenticator{&mockAuthenticator{ 395 identities: []string{"test-identity"}, 396 kubernetesInfo: ztunnelCaller, 397 }}, 398 ca: &mockca.FakeCA{ 399 SignedCert: []byte("cert"), 400 KeyCertBundle: util.NewKeyCertBundleFromPem(nil, nil, []byte("cert_chain"), []byte("root_cert")), 401 }, 402 certChain: []string{"cert", "cert_chain", "root_cert"}, 403 pods: []pod{ztunnelPod}, 404 impersonatePod: podSameNodeRemote, 405 callerClusterID: cluster.ID("fake"), 406 trustedNodeAccounts: allowZtunnel, 407 isMultiCluster: true, 408 remoteClusterPods: []pod{ztunnelPodRemote, podSameNodeRemote}, 409 code: codes.Unauthenticated, 410 }, 411 { 412 name: "Successful signing with impersonate identity from remote cluster", 413 authenticators: []security.Authenticator{&mockAuthenticator{ 414 identities: []string{"test-identity"}, 415 kubernetesInfo: ztunnelCallerRemote, 416 }}, 417 ca: &mockca.FakeCA{ 418 SignedCert: []byte("cert"), 419 KeyCertBundle: util.NewKeyCertBundleFromPem(nil, nil, []byte("cert_chain"), []byte("root_cert")), 420 }, 421 certChain: []string{"cert", "cert_chain", "root_cert"}, 422 pods: []pod{ztunnelPod, podSameNode}, 423 impersonatePod: podSameNodeRemote, 424 callerClusterID: cluster.ID("fake-remote"), 425 trustedNodeAccounts: allowZtunnel, 426 isMultiCluster: true, 427 remoteClusterPods: []pod{ztunnelPodRemote, podSameNodeRemote}, 428 code: codes.OK, 429 }, 430 } 431 432 for _, c := range testCases { 433 t.Run(c.name, func(t *testing.T) { 434 test.SetForTest(t, &features.CATrustedNodeAccounts, c.trustedNodeAccounts) 435 436 multiClusterController := multicluster.NewFakeController() 437 server, _ := New(c.ca, time.Duration(1), c.authenticators, multiClusterController) 438 439 var pods []runtime.Object 440 for _, p := range c.pods { 441 pods = append(pods, toPod(p, strings.HasPrefix(p.name, "ztunnel"))) 442 } 443 client := kube.NewFakeClient(pods...) 444 stop := test.NewStop(t) 445 multiClusterController.Add("fake", client, stop) 446 client.RunAndWait(stop) 447 448 if c.isMultiCluster { 449 var remoteClusterPods []runtime.Object 450 for _, p := range c.remoteClusterPods { 451 remoteClusterPods = append(remoteClusterPods, toPod(p, strings.HasPrefix(p.name, "ztunnel"))) 452 } 453 remoteClient := kube.NewFakeClient(remoteClusterPods...) 454 multiClusterController.Add("fake-remote", remoteClient, stop) 455 remoteClient.RunAndWait(stop) 456 } 457 458 if server.nodeAuthorizer != nil { 459 for _, c := range server.nodeAuthorizer.component.All() { 460 kube.WaitForCacheSync("test", stop, c.pods.HasSynced) 461 } 462 } 463 464 reqMeta, _ := structpb.NewStruct(map[string]any{ 465 security.ImpersonatedIdentity: c.impersonatePod.Identity(), 466 }) 467 request := &pb.IstioCertificateRequest{ 468 Csr: "dumb CSR", 469 Metadata: reqMeta, 470 } 471 472 p := &peer.Peer{Addr: &net.IPAddr{IP: net.IPv4(192, 168, 1, 1)}, AuthInfo: credentials.TLSInfo{}} 473 ctx := peer.NewContext(context.Background(), p) 474 if c.callerClusterID != "" { 475 ctx = metadata.NewIncomingContext(ctx, metadata.MD{ 476 "clusterid": []string{string(c.callerClusterID)}, 477 }) 478 } 479 480 response, err := server.CreateCertificate(ctx, request) 481 s, _ := status.FromError(err) 482 code := s.Code() 483 if c.code != code { 484 t.Errorf("Case %s: expecting code to be (%d) but got (%d): %s", c.name, c.code, code, s.Message()) 485 } else if c.code == codes.OK { 486 if len(response.CertChain) != len(c.certChain) { 487 t.Errorf("Case %s: expecting cert chain length to be (%d) but got (%d)", 488 c.name, len(c.certChain), len(response.CertChain)) 489 } 490 for i, v := range response.CertChain { 491 if v != c.certChain[i] { 492 t.Errorf("Case %s: expecting cert to be (%s) but got (%s) at position [%d] of cert chain.", 493 c.name, c.certChain, v, i) 494 } 495 } 496 497 } 498 }) 499 } 500 }