istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/cacert_rotation/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 cacertrotation
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  	"testing"
    24  	"time"
    25  
    26  	admin "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
    27  
    28  	"istio.io/istio/pkg/security"
    29  	"istio.io/istio/pkg/test/framework"
    30  	"istio.io/istio/pkg/test/framework/components/echo"
    31  	"istio.io/istio/pkg/test/framework/components/echo/check"
    32  	"istio.io/istio/pkg/test/framework/components/echo/common/deployment"
    33  	"istio.io/istio/pkg/test/framework/components/echo/echotest"
    34  	"istio.io/istio/pkg/test/framework/components/echo/match"
    35  	"istio.io/istio/pkg/test/framework/components/istio"
    36  	"istio.io/istio/pkg/test/framework/components/istioctl"
    37  	"istio.io/istio/pkg/test/framework/components/namespace"
    38  	"istio.io/istio/pkg/test/framework/label"
    39  	"istio.io/istio/pkg/test/framework/resource"
    40  	"istio.io/istio/pkg/test/util/retry"
    41  	"istio.io/istio/pkg/util/protomarshal"
    42  	"istio.io/istio/tests/integration/security/util/cert"
    43  	"istio.io/istio/tests/integration/security/util/reachability"
    44  )
    45  
    46  var apps deployment.SingleNamespaceView
    47  
    48  func TestMain(m *testing.M) {
    49  	framework.
    50  		NewSuite(m).
    51  		Label(label.CustomSetup).
    52  		Setup(istio.Setup(nil, setupConfig, cert.CreateCASecret)).
    53  		Setup(deployment.SetupSingleNamespace(&apps, deployment.Config{})).
    54  		Setup(func(ctx resource.Context) error {
    55  			return reachability.CreateCustomInstances(&apps)
    56  		}).
    57  		Run()
    58  }
    59  
    60  func setupConfig(_ resource.Context, cfg *istio.Config) {
    61  	if cfg == nil {
    62  		return
    63  	}
    64  	cfgYaml := `
    65  values:
    66    pilot:
    67      env:
    68        ISTIO_MULTIROOT_MESH: true
    69    meshConfig:
    70      defaultConfig:
    71        proxyMetadata:
    72          PROXY_CONFIG_XDS_AGENT: "true"
    73  `
    74  	cfg.ControlPlaneValues = cfgYaml
    75  }
    76  
    77  func TestReachability(t *testing.T) {
    78  	framework.NewTest(t).
    79  		Run(func(t framework.TestContext) {
    80  			istioCfg := istio.DefaultConfigOrFail(t, t)
    81  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
    82  			namespace.ClaimOrFail(t, t, istioCfg.SystemNamespace)
    83  
    84  			from := apps.EchoNamespace.A
    85  			to := apps.EchoNamespace.B
    86  			fromAndTo := from.Append(to)
    87  
    88  			lastUpdateTime, err := getWorkloadCertLastUpdateTime(t, from[0], istioCtl)
    89  			if err != nil {
    90  				t.Errorf("failed to get workload cert last update time: %v", err)
    91  			}
    92  
    93  			// Verify traffic works between a and b
    94  			echotest.New(t, fromAndTo).
    95  				WithDefaultFilters(1, 1).
    96  				FromMatch(match.ServiceName(from.NamespacedName())).
    97  				ToMatch(match.ServiceName(to.NamespacedName())).
    98  				Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
    99  					// Verify mTLS works between a and b
   100  					opts := echo.CallOptions{
   101  						To: to,
   102  						Port: echo.Port{
   103  							Name: "http",
   104  						},
   105  					}
   106  					opts.Check = check.And(check.OK(), check.ReachedTargetClusters(t))
   107  
   108  					from.CallOrFail(t, opts)
   109  				})
   110  
   111  			// step 1: Update CA root cert with combined root
   112  			if err := cert.CreateCustomCASecret(t,
   113  				"ca-cert.pem", "ca-key.pem",
   114  				"cert-chain.pem", "root-cert-combined.pem"); err != nil {
   115  				t.Errorf("failed to update combined CA secret: %v", err)
   116  			}
   117  
   118  			lastUpdateTime = waitForWorkloadCertUpdate(t, from[0], istioCtl, lastUpdateTime)
   119  
   120  			// step 2: Update CA signing key/cert with cacert to trigger workload cert resigning
   121  			if err := cert.CreateCustomCASecret(t,
   122  				"ca-cert-alt.pem", "ca-key-alt.pem",
   123  				"cert-chain-alt.pem", "root-cert-combined-2.pem"); err != nil {
   124  				t.Errorf("failed to update CA secret: %v", err)
   125  			}
   126  
   127  			lastUpdateTime = waitForWorkloadCertUpdate(t, from[0], istioCtl, lastUpdateTime)
   128  
   129  			// step 3: Remove the old root cert
   130  			if err := cert.CreateCustomCASecret(t,
   131  				"ca-cert-alt.pem", "ca-key-alt.pem",
   132  				"cert-chain-alt.pem", "root-cert-alt.pem"); err != nil {
   133  				t.Errorf("failed to update CA secret: %v", err)
   134  			}
   135  
   136  			waitForWorkloadCertUpdate(t, from[0], istioCtl, lastUpdateTime)
   137  
   138  			// Verify traffic works between a and b after cert rotation
   139  			echotest.New(t, fromAndTo).
   140  				WithDefaultFilters(1, 1).
   141  				FromMatch(match.ServiceName(from.NamespacedName())).
   142  				ToMatch(match.ServiceName(to.NamespacedName())).
   143  				Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
   144  					// Verify mTLS works between a and b
   145  					opts := echo.CallOptions{
   146  						To: to,
   147  						Port: echo.Port{
   148  							Name: "http",
   149  						},
   150  					}
   151  					opts.Check = check.And(check.OK(), check.ReachedTargetClusters(t))
   152  
   153  					from.CallOrFail(t, opts)
   154  				})
   155  		})
   156  }
   157  
   158  func getWorkloadCertLastUpdateTime(t framework.TestContext, i echo.Instance, ctl istioctl.Instance) (time.Time, error) {
   159  	podID, err := getPodID(i)
   160  	if err != nil {
   161  		t.Fatalf("Could not get Pod ID: %v", err)
   162  	}
   163  	podName := fmt.Sprintf("%s.%s", podID, i.NamespaceName())
   164  	out, errOut, err := ctl.Invoke([]string{"pc", "s", podName, "-o", "json"})
   165  	if err != nil || errOut != "" {
   166  		t.Errorf("failed to retrieve pod secret from %s, err: %v errOut: %s", podName, err, errOut)
   167  	}
   168  
   169  	dump := &admin.SecretsConfigDump{}
   170  	if err := protomarshal.Unmarshal([]byte(out), dump); err != nil {
   171  		t.Errorf("failed to unmarshal secret dump: %v", err)
   172  	}
   173  
   174  	for _, s := range dump.DynamicActiveSecrets {
   175  		if s.Name == security.WorkloadKeyCertResourceName {
   176  			return s.LastUpdated.AsTime(), nil
   177  		}
   178  	}
   179  
   180  	return time.Now(), errors.New("failed to find workload cert")
   181  }
   182  
   183  // Abstracted function to wait for workload cert to be updated
   184  func waitForWorkloadCertUpdate(t framework.TestContext, from echo.Instance, istioCtl istioctl.Instance, lastUpdateTime time.Time) time.Time {
   185  	retry.UntilOrFail(t, func() bool {
   186  		updateTime, err := getWorkloadCertLastUpdateTime(t, from, istioCtl)
   187  		if err != nil {
   188  			t.Logf("failed to get workload cert last update time: %v", err)
   189  			return false
   190  		}
   191  
   192  		// retry when workload cert is not updated
   193  		if updateTime.After(lastUpdateTime) {
   194  			lastUpdateTime = updateTime
   195  			t.Logf("workload cert is updated, last update time: %v", updateTime)
   196  			return true
   197  		}
   198  
   199  		return false
   200  	}, retry.Timeout(5*time.Minute), retry.Delay(1*time.Second))
   201  	return lastUpdateTime
   202  }
   203  
   204  func getPodID(i echo.Instance) (string, error) {
   205  	wls, err := i.Workloads()
   206  	if err != nil {
   207  		return "", nil
   208  	}
   209  
   210  	for _, wl := range wls {
   211  		return wl.PodName(), nil
   212  	}
   213  
   214  	return "", fmt.Errorf("no workloads")
   215  }