istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/ambient/cacert_rotation_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 ambient
    19  
    20  import (
    21  	"bytes"
    22  	"crypto/x509"
    23  	"encoding/base64"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	v1 "k8s.io/api/core/v1"
    32  
    33  	"istio.io/istio/istioctl/pkg/writer/ztunnel/configdump"
    34  	"istio.io/istio/pkg/test/framework"
    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  	kubetest "istio.io/istio/pkg/test/kube"
    39  	"istio.io/istio/pkg/test/util/assert"
    40  	"istio.io/istio/pkg/test/util/retry"
    41  	"istio.io/istio/security/pkg/pki/util"
    42  	"istio.io/istio/tests/integration/security/util/cert"
    43  )
    44  
    45  func TestIntermediateCertificateRefresh(t *testing.T) {
    46  	framework.NewTest(t).
    47  		Run(func(t framework.TestContext) {
    48  			t.Skip("https://github.com/istio/istio/issues/49648")
    49  			istioCfg := istio.DefaultConfigOrFail(t, t)
    50  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
    51  			namespace.ClaimOrFail(t, t, istioCfg.SystemNamespace)
    52  			newX509 := getX509FromFile(t, "ca-cert-alt-2.pem")
    53  
    54  			sa := apps.Captured[0].ServiceAccountName()
    55  
    56  			// we do not know which ztunnel instance is located on the node as the workload, so we need to check all of them initially
    57  			ztunnelPods, err := kubetest.NewPodFetch(t.AllClusters()[0], istioCfg.SystemNamespace, "app=ztunnel")()
    58  			assert.NoError(t, err)
    59  
    60  			originalWorkloadSecret, ztunnelPod, err := getWorkloadSecret(t, ztunnelPods, sa, istioCtl)
    61  			if err != nil {
    62  				t.Errorf("failed to get initial workload secret: %v", err)
    63  			}
    64  
    65  			// Update CA with new intermediate cert
    66  			if err := cert.CreateCustomCASecret(t,
    67  				"ca-cert-alt-2.pem", "ca-key-alt-2.pem",
    68  				"cert-chain-alt-2.pem", "root-cert-alt.pem"); err != nil {
    69  				t.Errorf("failed to update CA secret: %v", err)
    70  			}
    71  
    72  			// perform one retry to handle race condition where ztunnel cert is refreshed before Istiod certificates are reloaded
    73  			retry.UntilSuccess(func() error {
    74  				newWorkloadCert := waitForWorkloadCertUpdate(t, ztunnelPod, sa, istioCtl, originalWorkloadSecret)
    75  				return verifyWorkloadCert(t, newWorkloadCert, newX509)
    76  			}, retry.MaxAttempts(2), retry.Timeout(5*time.Minute))
    77  		})
    78  }
    79  
    80  func getWorkloadSecret(t framework.TestContext, zPods []v1.Pod, serviceAccount string, ctl istioctl.Instance) (*configdump.CertsDump, v1.Pod, error) {
    81  	for _, ztunnel := range zPods {
    82  		podName := fmt.Sprintf("%s.%s", ztunnel.Name, ztunnel.Namespace)
    83  		out, errOut, err := ctl.Invoke([]string{"pc", "s", podName, "-o", "json"})
    84  		if err != nil || errOut != "" {
    85  			t.Errorf("failed to retrieve pod secrets from %s, err: %v errOut: %s", podName, err, errOut)
    86  		}
    87  
    88  		dump := []configdump.CertsDump{}
    89  		if err := json.Unmarshal([]byte(out), &dump); err != nil {
    90  			t.Errorf("failed to unmarshal secret dump: %v", err)
    91  		}
    92  
    93  		for _, s := range dump {
    94  			if strings.Contains(s.Identity, serviceAccount) {
    95  				if len(s.CertChain) == 0 {
    96  					t.Fatalf("cert chain missing in %v for identity: %v", ztunnel.Name, s.Identity)
    97  				}
    98  				return &s, ztunnel, nil
    99  			}
   100  		}
   101  	}
   102  	return nil, v1.Pod{}, errors.New("failed to find workload secret")
   103  }
   104  
   105  // Abstracted function to wait for workload cert to be updated
   106  func waitForWorkloadCertUpdate(t framework.TestContext, ztunnelPod v1.Pod, serviceAccount string,
   107  	istioCtl istioctl.Instance, originalCert *configdump.CertsDump,
   108  ) *configdump.CertsDump {
   109  	var newSecret *configdump.CertsDump
   110  	retry.UntilOrFail(t, func() bool {
   111  		updatedCert, _, err := getWorkloadSecret(t, []v1.Pod{ztunnelPod}, serviceAccount, istioCtl)
   112  		if err != nil {
   113  			t.Logf("failed to get current workload secret: %v", err)
   114  			return false
   115  		}
   116  
   117  		// retry when workload cert is not updated
   118  		if originalCert.CertChain[0].ValidFrom != updatedCert.CertChain[0].ValidFrom {
   119  			newSecret = updatedCert
   120  			t.Logf("workload cert is updated")
   121  			return true
   122  		}
   123  
   124  		return false
   125  	}, retry.Timeout(5*time.Minute), retry.Delay(10*time.Second))
   126  	return newSecret
   127  }
   128  
   129  func verifyWorkloadCert(t framework.TestContext, workloadSecret *configdump.CertsDump, caX590 *x509.Certificate) error {
   130  	intermediateCert, err := base64.StdEncoding.DecodeString(workloadSecret.CertChain[1].Pem)
   131  	if err != nil {
   132  		t.Errorf("failed to decode intermediate certificate: %v", err)
   133  	}
   134  	intermediateX509 := parseCert(t, intermediateCert)
   135  	// verify the correct intermediate cert is in the certificate chain
   136  	if intermediateX509.SerialNumber.String() != caX590.SerialNumber.String() {
   137  		return fmt.Errorf("intermediate certificate serial numbers do not match: got %v, wanted %v",
   138  			intermediateX509.SerialNumber.String(), caX590.SerialNumber.String())
   139  	}
   140  
   141  	workloadCert, err := base64.StdEncoding.DecodeString(workloadSecret.CertChain[0].Pem)
   142  	if err != nil {
   143  		return fmt.Errorf("failed to decode workload certificate: %v", err)
   144  	}
   145  	workloadX509 := parseCert(t, workloadCert)
   146  
   147  	// verify workload cert contains the correct intermediate cert
   148  	if !bytes.Equal(workloadX509.AuthorityKeyId, caX590.SubjectKeyId) {
   149  		return fmt.Errorf("workload certificate did not have expected authority key id: got %v wanted %v",
   150  			string(workloadX509.AuthorityKeyId), string(caX590.SubjectKeyId))
   151  	}
   152  
   153  	return nil
   154  }
   155  
   156  func getX509FromFile(t framework.TestContext, caCertFile string) *x509.Certificate {
   157  	certBytes, err := cert.ReadSampleCertFromFile(caCertFile)
   158  	if err != nil {
   159  		t.Errorf("failed to read %s file: %v", caCertFile, err)
   160  	}
   161  	return parseCert(t, certBytes)
   162  }
   163  
   164  func parseCert(t framework.TestContext, certBytes []byte) *x509.Certificate {
   165  	parsedCert, err := util.ParsePemEncodedCertificate(certBytes)
   166  	if err != nil {
   167  		t.Errorf("failed to parse certificate pem file: %v", err)
   168  	}
   169  	return parsedCert
   170  }