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  }