istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/ca_custom_root/secure_naming_test.go (about)

     1  //go:build integ
     2  // +build integ
     3  
     4  // Copyright Istio Authors
     5  //
     6  // Licensed under the Apache License, Version 2.0 (the "License");
     7  // you may not use this file except in compliance with the License.
     8  // You may obtain a copy of the License at
     9  //
    10  //     http://www.apache.org/licenses/LICENSE-2.0
    11  //
    12  // Unless required by applicable law or agreed to in writing, software
    13  // distributed under the License is distributed on an "AS IS" BASIS,
    14  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  // See the License for the specific language governing permissions and
    16  // limitations under the License.
    17  
    18  package cacustomroot
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  
    29  	"istio.io/istio/pkg/config/constants"
    30  	"istio.io/istio/pkg/test/framework"
    31  	"istio.io/istio/pkg/test/framework/components/echo"
    32  	"istio.io/istio/pkg/test/framework/components/echo/check"
    33  	"istio.io/istio/pkg/test/framework/components/echo/match"
    34  	"istio.io/istio/pkg/test/framework/components/istio"
    35  	"istio.io/istio/pkg/test/framework/components/namespace"
    36  	"istio.io/istio/pkg/test/util/retry"
    37  	"istio.io/istio/tests/integration/security/util/cert"
    38  )
    39  
    40  const (
    41  	// The length of the example certificate chain.
    42  	exampleCertChainLength = 3
    43  
    44  	defaultIdentityDR = `apiVersion: networking.istio.io/v1alpha3
    45  kind: DestinationRule
    46  metadata:
    47    name: "service-b-dr"
    48  spec:
    49    host: "b.NS.svc.cluster.local"
    50    trafficPolicy:
    51      tls:
    52        mode: ISTIO_MUTUAL
    53        subjectAltNames:
    54        - "spiffe://cluster.local/ns/NS/sa/default"
    55  `
    56  	correctIdentityDR = `apiVersion: networking.istio.io/v1alpha3
    57  kind: DestinationRule
    58  metadata:
    59    name: "service-b-dr"
    60  spec:
    61    host: "b.NS.svc.cluster.local"
    62    trafficPolicy:
    63      tls:
    64        mode: ISTIO_MUTUAL
    65        subjectAltNames:
    66        - "spiffe://cluster.local/ns/NS/sa/b"
    67  `
    68  	nonExistIdentityDR = `apiVersion: networking.istio.io/v1alpha3
    69  kind: DestinationRule
    70  metadata:
    71    name: "service-b-dr"
    72  spec:
    73    host: "b.NS.svc.cluster.local"
    74    trafficPolicy:
    75      tls:
    76        mode: ISTIO_MUTUAL
    77        subjectAltNames:
    78        - "I-do-not-exist"
    79  `
    80  	identityListDR = `apiVersion: networking.istio.io/v1alpha3
    81  kind: DestinationRule
    82  metadata:
    83    name: "service-b-dr"
    84  spec:
    85    host: "b.NS.svc.cluster.local"
    86    trafficPolicy:
    87      tls:
    88        mode: ISTIO_MUTUAL
    89        subjectAltNames:
    90        - "spiffe://cluster.local/ns/NS/sa/a"
    91        - "spiffe://cluster.local/ns/NS/sa/b"
    92        - "spiffe://cluster.local/ns/default/sa/default"
    93        - "I-do-not-exist"
    94  `
    95  )
    96  
    97  // TestSecureNaming verifies:
    98  // - The certificate issued by CA to the sidecar is as expected and that strict mTLS works as expected.
    99  // - The plugin CA certs are correctly used in workload mTLS.
   100  // - The CA certificate in the configmap of each namespace is as expected, which
   101  //
   102  //	is used for data plane to control plane TLS authentication.
   103  //
   104  // - Secure naming information is respected in the mTLS handshake.
   105  func TestSecureNaming(t *testing.T) {
   106  	framework.NewTest(t).
   107  		Run(func(t framework.TestContext) {
   108  			istioCfg := istio.DefaultConfigOrFail(t, t)
   109  
   110  			testNamespace := apps.EchoNamespace.Namespace
   111  			namespace.ClaimOrFail(t, t, istioCfg.SystemNamespace)
   112  			// Check that the CA certificate in the configmap of each namespace is as expected, which
   113  			// is used for data plane to control plane TLS authentication.
   114  			retry.UntilSuccessOrFail(t, func() error {
   115  				return checkCACert(t, testNamespace)
   116  			}, retry.Delay(time.Second), retry.Timeout(10*time.Second))
   117  			to := match.Namespace(testNamespace).GetMatches(apps.EchoNamespace.B)
   118  			for _, cluster := range t.Clusters() {
   119  				t.NewSubTest(fmt.Sprintf("From %s", cluster.StableName())).Run(func(t framework.TestContext) {
   120  					a := match.And(match.Cluster(cluster), match.Namespace(testNamespace)).GetMatches(apps.EchoNamespace.A)[0]
   121  					b := match.And(match.Cluster(cluster), match.Namespace(testNamespace)).GetMatches(apps.EchoNamespace.B)[0]
   122  					t.NewSubTest("mTLS cert validation with plugin CA").
   123  						Run(func(t framework.TestContext) {
   124  							// Verify that the certificate issued to the sidecar is as expected.
   125  							out := cert.DumpCertFromSidecar(t, a, b, "http")
   126  							verifyCertificatesWithPluginCA(t, out)
   127  
   128  							// Verify mTLS works between a and b
   129  							opts := echo.CallOptions{
   130  								To: to,
   131  								Port: echo.Port{
   132  									Name: "http",
   133  								},
   134  							}
   135  							opts.Check = check.And(check.OK(), check.ReachedTargetClusters(t))
   136  							a.CallOrFail(t, opts)
   137  						})
   138  
   139  					secureNamingTestCases := []struct {
   140  						name            string
   141  						destinationRule string
   142  						expectSuccess   bool
   143  					}{
   144  						{
   145  							name:            "connection fails when DR doesn't match SA",
   146  							destinationRule: defaultIdentityDR,
   147  							expectSuccess:   false,
   148  						},
   149  						{
   150  							name:            "connection succeeds when DR matches SA",
   151  							destinationRule: correctIdentityDR,
   152  							expectSuccess:   true,
   153  						},
   154  						{
   155  							name:            "connection fails when DR contains non-matching, non-existing SA",
   156  							destinationRule: nonExistIdentityDR,
   157  							expectSuccess:   false,
   158  						},
   159  						{
   160  							name:            "connection succeeds when SA is in the list of SANs",
   161  							destinationRule: identityListDR,
   162  							expectSuccess:   true,
   163  						},
   164  					}
   165  					for _, tc := range secureNamingTestCases {
   166  						t.NewSubTest(tc.name).
   167  							Run(func(t framework.TestContext) {
   168  								dr := strings.ReplaceAll(tc.destinationRule, "NS", testNamespace.Name())
   169  								t.ConfigIstio().YAML(testNamespace.Name(), dr).ApplyOrFail(t)
   170  								// Verify mTLS works between a and b
   171  								opts := echo.CallOptions{
   172  									To: to,
   173  									Port: echo.Port{
   174  										Name: "http",
   175  									},
   176  								}
   177  								if tc.expectSuccess {
   178  									opts.Check = check.And(check.OK(), check.ReachedTargetClusters(t))
   179  								} else {
   180  									opts.Check = check.NotOK()
   181  								}
   182  
   183  								a.CallOrFail(t, opts)
   184  							})
   185  					}
   186  				})
   187  			}
   188  		})
   189  }
   190  
   191  func verifyCertificatesWithPluginCA(t framework.TestContext, certs []string) {
   192  	// Verify that the certificate chain length is as expected
   193  	if len(certs) != exampleCertChainLength {
   194  		t.Errorf("expect %v certs in the cert chain but getting %v certs",
   195  			exampleCertChainLength, len(certs))
   196  		return
   197  	}
   198  	var rootCert []byte
   199  	var err error
   200  	if rootCert, err = cert.ReadSampleCertFromFile("root-cert.pem"); err != nil {
   201  		t.Errorf("error when reading expected CA cert: %v", err)
   202  		return
   203  	}
   204  	// Verify that the CA certificate is as expected
   205  	if strings.TrimSpace(string(rootCert)) != strings.TrimSpace(certs[2]) {
   206  		t.Errorf("the actual CA cert is different from the expected. expected: %v, actual: %v",
   207  			strings.TrimSpace(string(rootCert)), strings.TrimSpace(certs[2]))
   208  		return
   209  	}
   210  	t.Log("the CA certificate is as expected")
   211  }
   212  
   213  func checkCACert(t framework.TestContext, testNamespace namespace.Instance) error {
   214  	configMapName := "istio-ca-root-cert"
   215  	cm, err := t.Clusters().Default().Kube().CoreV1().ConfigMaps(testNamespace.Name()).Get(context.TODO(), configMapName,
   216  		metav1.GetOptions{})
   217  	if err != nil {
   218  		return err
   219  	}
   220  	var certData string
   221  	var pluginCert []byte
   222  	var ok bool
   223  	if certData, ok = cm.Data[constants.CACertNamespaceConfigMapDataName]; !ok {
   224  		return fmt.Errorf("CA certificate %v not found", constants.CACertNamespaceConfigMapDataName)
   225  	}
   226  	t.Logf("CA certificate %v found", constants.CACertNamespaceConfigMapDataName)
   227  	if pluginCert, err = cert.ReadSampleCertFromFile("root-cert.pem"); err != nil {
   228  		return err
   229  	}
   230  	if string(pluginCert) != certData {
   231  		return fmt.Errorf("CA certificate (%v) not matching plugin cert (%v)", certData, string(pluginCert))
   232  	}
   233  
   234  	return nil
   235  }