istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/file_mounted_certs/main_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 filemountedcerts
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"os"
    24  	"path"
    25  	"strings"
    26  	"testing"
    27  
    28  	v1 "k8s.io/api/core/v1"
    29  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  
    32  	"istio.io/api/annotation"
    33  	"istio.io/istio/pkg/config/protocol"
    34  	"istio.io/istio/pkg/test/env"
    35  	"istio.io/istio/pkg/test/framework"
    36  	"istio.io/istio/pkg/test/framework/components/echo"
    37  	"istio.io/istio/pkg/test/framework/components/echo/common/deployment"
    38  	"istio.io/istio/pkg/test/framework/components/istio"
    39  	"istio.io/istio/pkg/test/framework/components/namespace"
    40  	"istio.io/istio/pkg/test/framework/label"
    41  	"istio.io/istio/pkg/test/framework/resource"
    42  )
    43  
    44  var (
    45  	inst         istio.Instance
    46  	apps         deployment.SingleNamespaceView
    47  	client       echo.Instances
    48  	server       echo.Instances
    49  	echo1NS      namespace.Instance
    50  	customConfig []echo.Config
    51  )
    52  
    53  const (
    54  	PilotCertsPath  = "tests/testdata/certs/pilot"
    55  	PilotSecretName = "test-istiod-server-cred"
    56  
    57  	ProxyMetadataJSON = `
    58  	{
    59        "FILE_MOUNTED_CERTS": "true",
    60        "ISTIO_META_TLS_CLIENT_CERT_CHAIN": "/client-certs/cert-chain.pem",
    61        "ISTIO_META_TLS_CLIENT_KEY": "/client-certs/key.pem",
    62        "ISTIO_META_TLS_CLIENT_ROOT_CERT": "/client-certs/root-cert.pem",
    63        "ISTIO_META_TLS_SERVER_CERT_CHAIN": "/server-certs/cert-chain.pem",
    64        "ISTIO_META_TLS_SERVER_KEY": "/server-certs/key.pem",
    65        "ISTIO_META_TLS_SERVER_ROOT_CERT": "/server-certs/root-cert.pem",
    66  	}
    67  `
    68  )
    69  
    70  func TestMain(m *testing.M) {
    71  	// nolint: staticcheck
    72  	framework.
    73  		NewSuite(m).
    74  		Label(label.CustomSetup).
    75  		RequireSingleCluster().
    76  		RequireMultiPrimary().
    77  		Setup(istio.Setup(&inst, setupConfig, CreateCustomIstiodSecret)).
    78  		Setup(namespace.Setup(&echo1NS, namespace.Config{Prefix: "echo1", Inject: true})).
    79  		Setup(func(ctx resource.Context) error {
    80  			err := setupApps(ctx, namespace.Future(&echo1NS), &customConfig)
    81  			if err != nil {
    82  				return err
    83  			}
    84  			return nil
    85  		}).
    86  		Setup(deployment.SetupSingleNamespace(&apps, deployment.Config{
    87  			Namespaces: []namespace.Getter{
    88  				namespace.Future(&echo1NS),
    89  			},
    90  			Configs: echo.ConfigFuture(&customConfig),
    91  		})).
    92  		Setup(func(ctx resource.Context) error {
    93  			return createCustomInstances(&apps)
    94  		}).
    95  		Run()
    96  }
    97  
    98  func setupConfig(_ resource.Context, cfg *istio.Config) {
    99  	if cfg == nil {
   100  		return
   101  	}
   102  
   103  	cfg.ControlPlaneValues = `
   104  components:
   105    egressGateways:
   106    - enabled: true
   107      name: istio-egressgateway
   108      k8s:
   109        overlays:
   110          - kind: Deployment
   111            name: istio-egressgateway
   112            patches:
   113              - path: spec.template.spec.volumes[100]
   114                value: |-
   115                  name: server-certs
   116                  secret:
   117                    secretName: ` + PilotSecretName + `
   118                    defaultMode: 420
   119              - path: spec.template.spec.volumes[101]
   120                value: |-
   121                  name: client-certs
   122                  secret:
   123                    secretName: ` + PilotSecretName + `
   124                    defaultMode: 420
   125              - path: spec.template.spec.containers[0].volumeMounts[100]
   126                value: |-
   127                  name: server-certs
   128                  mountPath: /server-certs
   129              - path: spec.template.spec.containers[0].volumeMounts[101]
   130                value: |-
   131                  name: client-certs
   132                  mountPath: /client-certs
   133  
   134    ingressGateways:
   135    - enabled: true
   136      name: istio-ingressgateway
   137      k8s:
   138        overlays:
   139          - kind: Deployment
   140            name: istio-ingressgateway
   141            patches:
   142              - path: spec.template.spec.volumes[100]
   143                value: |-
   144                  name: server-certs
   145                  secret:
   146                    secretName: ` + PilotSecretName + `
   147                    defaultMode: 420
   148              - path: spec.template.spec.volumes[101]
   149                value: |-
   150                  name: client-certs
   151                  secret:
   152                    secretName: ` + PilotSecretName + `
   153                    defaultMode: 420
   154              - path: spec.template.spec.containers[0].volumeMounts[100]
   155                value: |-
   156                  name: server-certs
   157                  mountPath: /server-certs
   158              - path: spec.template.spec.containers[0].volumeMounts[101]
   159                value: |-
   160                  name: client-certs
   161                  mountPath: /client-certs
   162  
   163    pilot:
   164      enabled: true
   165      k8s:
   166        overlays:
   167          - kind: Deployment
   168            name: istiod
   169            patches:
   170              - path: spec.template.spec.containers.[name:discovery].args[1001]
   171                value: "--caCertFile=/server-certs/root-cert.pem"
   172              - path: spec.template.spec.containers.[name:discovery].args[1002]
   173                value: "--tlsCertFile=/server-certs/cert-chain.pem"
   174              - path: spec.template.spec.containers.[name:discovery].args[1003]
   175                value: "--tlsKeyFile=/server-certs/key.pem"
   176              - path: spec.template.spec.volumes[-1]
   177                value: |-
   178                  name: server-certs
   179                  secret:
   180                    secretName: ` + PilotSecretName + `
   181                    defaultMode: 420
   182              - path: spec.template.spec.containers[name:discovery].volumeMounts[-1]
   183                value: |-
   184                  name: server-certs
   185                  mountPath: /server-certs
   186  
   187  meshConfig:
   188    defaultConfig:
   189      controlPlaneAuthPolicy: "MUTUAL_TLS"
   190      proxyMetadata: ` + strings.Replace(ProxyMetadataJSON, "\n", "", -1) +
   191  		`
   192  values:
   193    global:
   194      pilotCertProvider: "mycopki"
   195    pilot:
   196      env:
   197        # We need to turn off the XDS Auth because test certificates only have a fixed/hardcoded identity, but the identity of the actual
   198        # deployed test services changes on each run due to a randomly generated namespace suffixes.
   199        # Turning the XDS-Auth ON will result in the error messages like:
   200        # Unauthorized XDS: 10.1.0.159:41960 with identity [spiffe://cluster.local/ns/mounted-certs/sa/client client.mounted-certs.svc]:
   201        #    no identities ([spiffe://cluster.local/ns/mounted-certs/sa/client client.mounted-certs.svc]) matched istio-fd-sds-1-4523/default
   202        XDS_AUTH: "false"
   203  `
   204  
   205  	cfg.EastWestGatewayValues = `
   206  components:
   207    ingressGateways:
   208    - enabled: true
   209      name: istio-eastwestgateway
   210      label:
   211        istio: eastwestgateway
   212        app: istio-eastwestgateway
   213      k8s:
   214        overlays:
   215          - kind: Deployment
   216            name: istio-eastwestgateway
   217            patches:
   218              - path: spec.template.spec.volumes[100]
   219                value: |-
   220                  name: server-certs
   221                  secret:
   222                    secretName: ` + PilotSecretName + `
   223                    defaultMode: 420
   224              - path: spec.template.spec.volumes[101]
   225                value: |-
   226                  name: client-certs
   227                  secret:
   228                    secretName: ` + PilotSecretName + `
   229                    defaultMode: 420
   230              - path: spec.template.spec.containers[0].volumeMounts[100]
   231                value: |-
   232                  name: server-certs
   233                  mountPath: /server-certs
   234              - path: spec.template.spec.containers[0].volumeMounts[101]
   235                value: |-
   236                  name: client-certs
   237                  mountPath: /client-certs
   238  `
   239  }
   240  
   241  func CreateCustomIstiodSecret(ctx resource.Context) error {
   242  	systemNs, err := istio.ClaimSystemNamespace(ctx)
   243  	if err != nil {
   244  		return err
   245  	}
   246  
   247  	err = CreateCustomSecret(ctx, PilotSecretName, systemNs, PilotCertsPath)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	return nil
   253  }
   254  
   255  func CreateCustomSecret(ctx resource.Context, name string, namespace namespace.Instance, certsPath string) error {
   256  	var privateKey, clientCert, caCert []byte
   257  	var err error
   258  	if privateKey, err = ReadCustomCertFromFile(certsPath, "key.pem"); err != nil {
   259  		return err
   260  	}
   261  	if clientCert, err = ReadCustomCertFromFile(certsPath, "cert-chain.pem"); err != nil {
   262  		return err
   263  	}
   264  	if caCert, err = ReadCustomCertFromFile(certsPath, "root-cert.pem"); err != nil {
   265  		return err
   266  	}
   267  
   268  	kubeAccessor := ctx.Clusters().Default()
   269  	secret := &v1.Secret{
   270  		ObjectMeta: metav1.ObjectMeta{
   271  			Name:      name,
   272  			Namespace: namespace.Name(),
   273  		},
   274  		Data: map[string][]byte{
   275  			"key.pem":        privateKey,
   276  			"cert-chain.pem": clientCert,
   277  			"root-cert.pem":  caCert,
   278  		},
   279  	}
   280  
   281  	_, err = kubeAccessor.Kube().CoreV1().Secrets(namespace.Name()).Create(context.TODO(), secret, metav1.CreateOptions{})
   282  	if err != nil {
   283  		if kerrors.IsAlreadyExists(err) {
   284  			if _, err := kubeAccessor.Kube().CoreV1().Secrets(namespace.Name()).Update(context.TODO(), secret, metav1.UpdateOptions{}); err != nil {
   285  				return fmt.Errorf("failed updating secret %s: %v", secret.Name, err)
   286  			}
   287  		} else {
   288  			return fmt.Errorf("failed creating secret %s: %v", secret.Name, err)
   289  		}
   290  	}
   291  	return nil
   292  }
   293  
   294  func ReadCustomCertFromFile(certsPath string, f string) ([]byte, error) {
   295  	b, err := os.ReadFile(path.Join(env.IstioSrc, certsPath, f))
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	return b, nil
   300  }
   301  
   302  func setupApps(ctx resource.Context, customNs namespace.Getter, customCfg *[]echo.Config) error {
   303  	appsNamespace := customNs.Get()
   304  
   305  	// Server certificate has "server.file-mounted.svc" in SANs; Same is expected in DestinationRule.subjectAltNames for the test Echo server
   306  	// This cert is going to be used as a server and "client" certificate on the "Echo Server"'s side
   307  	err := CreateCustomSecret(ctx, ServerSecretName, appsNamespace, ServerCertsPath)
   308  	if err != nil {
   309  		return fmt.Errorf("Unable to create server secret. %v", err)
   310  	}
   311  
   312  	// Pilot secret will be used for xds connections from echo-server & echo-client to the control plane.
   313  	err = CreateCustomSecret(ctx, PilotSecretName, appsNamespace, PilotCertsPath)
   314  	if err != nil {
   315  		return fmt.Errorf("Unable to create pilot secret. %v", err)
   316  	}
   317  
   318  	// Client secret will be used as a "server" and client certificate on the "Echo Client"'s side.
   319  	// ie. it is going to be used for connections from EchoClient to EchoServer
   320  	err = CreateCustomSecret(ctx, ClientSecretName, appsNamespace, ClientCertsPath)
   321  	if err != nil {
   322  		return fmt.Errorf("Unable to create client secret. %v", err)
   323  	}
   324  
   325  	clientSidecarVolumes := `
   326  		{
   327  			"server-certs": {"secret": {"secretName":"` + ClientSecretName + `"}},
   328  			"client-certs": {"secret": {"secretName":"` + ClientSecretName + `"}},
   329  			"workload-certs": {"secret": {"secretName": "` + ClientSecretName + `"}}
   330  		}
   331  	`
   332  
   333  	serverSidecarVolumes := `
   334  		{
   335  			"server-certs": {"secret": {"secretName":"` + ServerSecretName + `"}},
   336  			"client-certs": {"secret": {"secretName":"` + ServerSecretName + `"}},
   337  			"workload-certs": {"secret": {"secretName":"` + ServerSecretName + `"}}
   338  		}
   339  	`
   340  
   341  	// workload-certs are needed in order to load the "default" SDS resource, which
   342  	// will be used for the xds-grpc mTLS (tls_certificate_sds_secret_configs.name == "default")
   343  	sidecarVolumeMounts := `
   344  		{
   345  			"server-certs": {
   346  				"mountPath": "/server-certs"
   347  			},
   348  			"client-certs": {
   349  				"mountPath": "/client-certs"
   350  			},
   351  			"workload-certs": {
   352  				"mountPath": "/etc/certs"
   353  			}
   354  		}
   355  	`
   356  
   357  	var customConfig []echo.Config
   358  
   359  	client := echo.Config{
   360  		Service:   "client",
   361  		Namespace: appsNamespace,
   362  		Ports:     []echo.Port{},
   363  		Subsets: []echo.SubsetConfig{{
   364  			Version: "v1",
   365  			// Set up custom annotations to mount the certs.
   366  			Annotations: map[string]string{
   367  				annotation.SidecarUserVolume.Name:      clientSidecarVolumes,
   368  				annotation.SidecarUserVolumeMount.Name: sidecarVolumeMounts,
   369  				// the default bootstrap template does not support reusing values from the `ISTIO_META_TLS_CLIENT_*` environment variables
   370  				// see security/pkg/nodeagent/cache/secretcache.go:generateFileSecret() for details
   371  				annotation.ProxyConfig.Name: `{"controlPlaneAuthPolicy":"MUTUAL_TLS","proxyMetadata":` + strings.Replace(ProxyMetadataJSON, "\n", "", -1) + `}`,
   372  			},
   373  		}},
   374  	}
   375  
   376  	server := echo.Config{
   377  		Service:   "server",
   378  		Namespace: appsNamespace,
   379  		Ports: []echo.Port{
   380  			{
   381  				Name:         "http",
   382  				Protocol:     protocol.HTTP,
   383  				ServicePort:  8443,
   384  				WorkloadPort: 8443,
   385  				TLS:          false,
   386  			},
   387  		},
   388  		Subsets: []echo.SubsetConfig{{
   389  			Version: "v1",
   390  			// Set up custom annotations to mount the certs.
   391  			Annotations: map[string]string{
   392  				annotation.SidecarUserVolume.Name:      serverSidecarVolumes,
   393  				annotation.SidecarUserVolumeMount.Name: sidecarVolumeMounts,
   394  				// the default bootstrap template does not support reusing values from the `ISTIO_META_TLS_CLIENT_*` environment variables
   395  				// see security/pkg/nodeagent/cache/secretcache.go:generateFileSecret() for details
   396  				annotation.ProxyConfig.Name: `{"controlPlaneAuthPolicy":"MUTUAL_TLS","proxyMetadata":` + strings.Replace(ProxyMetadataJSON, "\n", "", -1) + `}`,
   397  			},
   398  		}},
   399  	}
   400  	customConfig = append(customConfig, client, server)
   401  	*customCfg = customConfig
   402  	return nil
   403  }
   404  
   405  func createCustomInstances(apps *deployment.SingleNamespaceView) error {
   406  	for index, namespacedName := range apps.EchoNamespace.All.NamespacedNames() {
   407  		switch {
   408  		case namespacedName.Name == "client":
   409  			client = apps.EchoNamespace.All[index]
   410  		case namespacedName.Name == "server":
   411  			server = apps.EchoNamespace.All[index]
   412  		}
   413  	}
   414  	return nil
   415  }