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 }