istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/k8s/chiron/utils_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package chiron 16 17 import ( 18 "bytes" 19 "fmt" 20 "net/http" 21 "net/http/httptest" 22 "os" 23 "path/filepath" 24 "strconv" 25 "strings" 26 "testing" 27 "time" 28 29 cert "k8s.io/api/certificates/v1" 30 corev1 "k8s.io/api/core/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/client-go/kubernetes/fake" 34 kt "k8s.io/client-go/testing" 35 36 "istio.io/istio/pkg/kube" 37 "istio.io/istio/pkg/log" 38 "istio.io/istio/pkg/test" 39 csrctrl "istio.io/istio/pkg/test/csrctrl/controllers" 40 "istio.io/istio/pkg/test/util/assert" 41 pkiutil "istio.io/istio/security/pkg/pki/util" 42 ) 43 44 const ( 45 // exampleCACert copied from samples/certs/ca-cert.pem 46 exampleCACert = `-----BEGIN CERTIFICATE----- 47 MIIDnzCCAoegAwIBAgIJAON1ifrBZ2/BMA0GCSqGSIb3DQEBCwUAMIGLMQswCQYD 48 VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJU3Vubnl2YWxl 49 MQ4wDAYDVQQKDAVJc3RpbzENMAsGA1UECwwEVGVzdDEQMA4GA1UEAwwHUm9vdCBD 50 QTEiMCAGCSqGSIb3DQEJARYTdGVzdHJvb3RjYUBpc3Rpby5pbzAgFw0xODAxMjQx 51 OTE1NTFaGA8yMTE3MTIzMTE5MTU1MVowWTELMAkGA1UEBhMCVVMxEzARBgNVBAgT 52 CkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTEOMAwGA1UEChMFSXN0aW8x 53 ETAPBgNVBAMTCElzdGlvIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC 54 AQEAyzCxr/xu0zy5rVBiso9ffgl00bRKvB/HF4AX9/ytmZ6Hqsy13XIQk8/u/By9 55 iCvVwXIMvyT0CbiJq/aPEj5mJUy0lzbrUs13oneXqrPXf7ir3HzdRw+SBhXlsh9z 56 APZJXcF93DJU3GabPKwBvGJ0IVMJPIFCuDIPwW4kFAI7R/8A5LSdPrFx6EyMXl7K 57 M8jekC0y9DnTj83/fY72WcWX7YTpgZeBHAeeQOPTZ2KYbFal2gLsar69PgFS0Tom 58 ESO9M14Yit7mzB1WDK2z9g3r+zLxENdJ5JG/ZskKe+TO4Diqi5OJt/h8yspS1ck8 59 LJtCole9919umByg5oruflqIlQIDAQABozUwMzALBgNVHQ8EBAMCAgQwDAYDVR0T 60 BAUwAwEB/zAWBgNVHREEDzANggtjYS5pc3Rpby5pbzANBgkqhkiG9w0BAQsFAAOC 61 AQEAltHEhhyAsve4K4bLgBXtHwWzo6SpFzdAfXpLShpOJNtQNERb3qg6iUGQdY+w 62 A2BpmSkKr3Rw/6ClP5+cCG7fGocPaZh+c+4Nxm9suMuZBZCtNOeYOMIfvCPcCS+8 63 PQ/0hC4/0J3WJKzGBssaaMufJxzgFPPtDJ998kY8rlROghdSaVt423/jXIAYnP3Y 64 05n8TGERBj7TLdtIVbtUIx3JHAo3PWJywA6mEDovFMJhJERp9sDHIr1BbhXK1TFN 65 Z6HNH6gInkSSMtvC4Ptejb749PTaePRPF7ID//eq/3AH8UK50F3TQcLjEqWUsJUn 66 aFKltOc+RAjzDklcUPeG4Y6eMA== 67 -----END CERTIFICATE-----` 68 69 // exampleIssuedCert copied from samples/certs/cert-chain.pem 70 exampleIssuedCert = `-----BEGIN CERTIFICATE----- 71 MIIDnzCCAoegAwIBAgIJAON1ifrBZ2/BMA0GCSqGSIb3DQEBCwUAMIGLMQswCQYD 72 VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJU3Vubnl2YWxl 73 MQ4wDAYDVQQKDAVJc3RpbzENMAsGA1UECwwEVGVzdDEQMA4GA1UEAwwHUm9vdCBD 74 QTEiMCAGCSqGSIb3DQEJARYTdGVzdHJvb3RjYUBpc3Rpby5pbzAgFw0xODAxMjQx 75 OTE1NTFaGA8yMTE3MTIzMTE5MTU1MVowWTELMAkGA1UEBhMCVVMxEzARBgNVBAgT 76 CkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTEOMAwGA1UEChMFSXN0aW8x 77 ETAPBgNVBAMTCElzdGlvIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC 78 AQEAyzCxr/xu0zy5rVBiso9ffgl00bRKvB/HF4AX9/ytmZ6Hqsy13XIQk8/u/By9 79 iCvVwXIMvyT0CbiJq/aPEj5mJUy0lzbrUs13oneXqrPXf7ir3HzdRw+SBhXlsh9z 80 APZJXcF93DJU3GabPKwBvGJ0IVMJPIFCuDIPwW4kFAI7R/8A5LSdPrFx6EyMXl7K 81 M8jekC0y9DnTj83/fY72WcWX7YTpgZeBHAeeQOPTZ2KYbFal2gLsar69PgFS0Tom 82 ESO9M14Yit7mzB1WDK2z9g3r+zLxENdJ5JG/ZskKe+TO4Diqi5OJt/h8yspS1ck8 83 LJtCole9919umByg5oruflqIlQIDAQABozUwMzALBgNVHQ8EBAMCAgQwDAYDVR0T 84 BAUwAwEB/zAWBgNVHREEDzANggtjYS5pc3Rpby5pbzANBgkqhkiG9w0BAQsFAAOC 85 AQEAltHEhhyAsve4K4bLgBXtHwWzo6SpFzdAfXpLShpOJNtQNERb3qg6iUGQdY+w 86 A2BpmSkKr3Rw/6ClP5+cCG7fGocPaZh+c+4Nxm9suMuZBZCtNOeYOMIfvCPcCS+8 87 PQ/0hC4/0J3WJKzGBssaaMufJxzgFPPtDJ998kY8rlROghdSaVt423/jXIAYnP3Y 88 05n8TGERBj7TLdtIVbtUIx3JHAo3PWJywA6mEDovFMJhJERp9sDHIr1BbhXK1TFN 89 Z6HNH6gInkSSMtvC4Ptejb749PTaePRPF7ID//eq/3AH8UK50F3TQcLjEqWUsJUn 90 aFKltOc+RAjzDklcUPeG4Y6eMA== 91 -----END CERTIFICATE----- 92 ` 93 DefaulCertTTL = 24 * time.Hour 94 ) 95 96 type mockTLSServer struct { 97 httpServer *httptest.Server 98 } 99 100 func defaultReactionFunc(obj runtime.Object) kt.ReactionFunc { 101 return func(act kt.Action) (bool, runtime.Object, error) { 102 return true, obj, nil 103 } 104 } 105 106 const testSigner = "test-signer" 107 108 func runTestSigner(t test.Failer) ([]csrctrl.SignerRootCert, kube.CLIClient) { 109 c := kube.NewFakeClient() 110 signers, err := csrctrl.RunCSRController(testSigner, test.NewStop(t), []kube.Client{c}) 111 if err != nil { 112 t.Fatal(err) 113 } 114 return signers, c 115 } 116 117 func TestGenKeyCertK8sCA(t *testing.T) { 118 log.FindScope("default").SetOutputLevel(log.DebugLevel) 119 signers, client := runTestSigner(t) 120 ca := filepath.Join(t.TempDir(), "root-cert.pem") 121 os.WriteFile(ca, []byte(signers[0].Rootcert), 0o666) 122 123 _, _, _, err := GenKeyCertK8sCA(client.Kube(), "foo", ca, testSigner, true, DefaulCertTTL) 124 assert.NoError(t, err) 125 } 126 127 func TestReadCACert(t *testing.T) { 128 testCases := map[string]struct { 129 certPath string 130 shouldFail bool 131 expectedCert []byte 132 }{ 133 "cert not exist": { 134 certPath: "./invalid-path/invalid-file", 135 shouldFail: true, 136 }, 137 "cert valid": { 138 certPath: "./test-data/example-ca-cert.pem", 139 shouldFail: false, 140 expectedCert: []byte(exampleCACert), 141 }, 142 "cert invalid": { 143 certPath: "./test-data/example-invalid-ca-cert.pem", 144 shouldFail: true, 145 }, 146 } 147 148 for _, tc := range testCases { 149 t.Run(tc.certPath, func(t *testing.T) { 150 cert, err := readCACert(tc.certPath) 151 if tc.shouldFail { 152 if err == nil { 153 t.Errorf("should have failed at readCACert()") 154 } else { 155 // Should fail, skip the current case. 156 return 157 } 158 } else if err != nil { 159 t.Errorf("failed at readCACert(): %v", err) 160 } 161 162 if !bytes.Equal(tc.expectedCert, cert) { 163 t.Error("the certificate read is unexpected") 164 } 165 }) 166 } 167 } 168 169 func TestIsTCPReachable(t *testing.T) { 170 server1 := newMockTLSServer(t) 171 defer server1.httpServer.Close() 172 server2 := newMockTLSServer(t) 173 defer server2.httpServer.Close() 174 175 host := "127.0.0.1" 176 port1, err := getServerPort(server1.httpServer) 177 if err != nil { 178 t.Fatalf("error to get the server 1 port: %v", err) 179 } 180 port2, err := getServerPort(server2.httpServer) 181 if err != nil { 182 t.Fatalf("error to get the server 2 port: %v", err) 183 } 184 185 // Server 1 should be reachable, since it is not closed. 186 if !isTCPReachable(host, port1) { 187 t.Fatal("server 1 is unreachable") 188 } 189 190 // After closing server 2, server 2 should not be reachable 191 server2.httpServer.Close() 192 if isTCPReachable(host, port2) { 193 t.Fatal("server 2 is reachable") 194 } 195 } 196 197 func TestSubmitCSR(t *testing.T) { 198 testCases := map[string]struct { 199 gracePeriodRatio float32 200 minGracePeriod time.Duration 201 k8sCaCertFile string 202 dnsNames []string 203 secretNames []string 204 205 secretName string 206 secretNameSpace string 207 expectFail bool 208 }{ 209 "submitting a CSR without duplicate should succeed": { 210 gracePeriodRatio: 0.6, 211 k8sCaCertFile: "./test-data/example-ca-cert.pem", 212 dnsNames: []string{"foo"}, 213 secretNames: []string{"istio.webhook.foo"}, 214 secretName: "mock-secret", 215 secretNameSpace: "mock-secret-namespace", 216 expectFail: false, 217 }, 218 } 219 220 for tcName, tc := range testCases { 221 t.Run(tcName, func(t *testing.T) { 222 client := fake.NewSimpleClientset() 223 csr := &cert.CertificateSigningRequest{ 224 ObjectMeta: metav1.ObjectMeta{ 225 Name: "domain-cluster.local-ns--secret-mock-secret", 226 }, 227 Status: cert.CertificateSigningRequestStatus{ 228 Certificate: []byte(exampleIssuedCert), 229 }, 230 } 231 client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr)) 232 233 usages := []cert.KeyUsage{ 234 cert.UsageDigitalSignature, 235 cert.UsageKeyEncipherment, 236 cert.UsageServerAuth, 237 cert.UsageClientAuth, 238 } 239 r, err := submitCSR(client, []byte("test-pem"), "test-signer", 240 usages, DefaulCertTTL) 241 if tc.expectFail { 242 assert.Error(t, err) 243 } else if err != nil || r == nil { 244 t.Errorf("test case (%s) failed unexpectedly: %v", tcName, err) 245 } 246 }) 247 } 248 } 249 250 func TestReadSignedCertificate(t *testing.T) { 251 testCases := []struct { 252 name string 253 gracePeriodRatio float32 254 minGracePeriod time.Duration 255 k8sCaCertFile string 256 secretNames []string 257 dnsNames []string 258 serviceNamespaces []string 259 260 secretName string 261 secretNameSpace string 262 263 invalidCert bool 264 expectFail bool 265 certificateData []byte 266 }{ 267 { 268 name: "read signed cert should succeed", 269 gracePeriodRatio: 0.6, 270 k8sCaCertFile: "./test-data/example-ca-cert.pem", 271 dnsNames: []string{"foo"}, 272 secretNames: []string{"istio.webhook.foo"}, 273 serviceNamespaces: []string{"foo.ns"}, 274 secretName: "mock-secret", 275 secretNameSpace: "mock-secret-namespace", 276 invalidCert: false, 277 expectFail: false, 278 certificateData: []byte(exampleIssuedCert), 279 }, 280 { 281 name: "read invalid signed cert should fail", 282 gracePeriodRatio: 0.6, 283 k8sCaCertFile: "./test-data/example-ca-cert.pem", 284 dnsNames: []string{"foo"}, 285 secretNames: []string{"istio.webhook.foo"}, 286 serviceNamespaces: []string{"foo.ns"}, 287 secretName: "mock-secret", 288 secretNameSpace: "mock-secret-namespace", 289 invalidCert: true, 290 expectFail: true, 291 certificateData: []byte("invalid-cert"), 292 }, 293 { 294 name: "read empty signed cert should fail", 295 gracePeriodRatio: 0.6, 296 k8sCaCertFile: "./test-data/example-ca-cert.pem", 297 dnsNames: []string{"foo"}, 298 secretNames: []string{"istio.webhook.foo"}, 299 serviceNamespaces: []string{"foo.ns"}, 300 secretName: "mock-secret", 301 secretNameSpace: "mock-secret-namespace", 302 invalidCert: true, 303 expectFail: true, 304 certificateData: []byte(""), 305 }, 306 } 307 308 for _, tc := range testCases { 309 t.Run(tc.name, func(t *testing.T) { 310 log.FindScope("default").SetOutputLevel(log.DebugLevel) 311 client := initFakeKubeClient(t, tc.certificateData) 312 313 // 4. Read the signed certificate 314 _, _, err := SignCSRK8s(client.Kube(), createFakeCsr(t), "fake-signer", []cert.KeyUsage{cert.UsageAny}, "fake.com", 315 tc.k8sCaCertFile, true, true, 1*time.Second) 316 317 if tc.expectFail { 318 if err == nil { 319 t.Fatalf("should have failed at updateMutatingWebhookConfig") 320 } 321 } else if err != nil { 322 t.Fatalf("failed at updateMutatingWebhookConfig: %v", err) 323 } 324 }) 325 } 326 } 327 328 func createFakeCsr(t *testing.T) []byte { 329 options := pkiutil.CertOptions{ 330 Host: "fake.com", 331 RSAKeySize: 2048, 332 PKCS8Key: false, 333 ECSigAlg: pkiutil.SupportedECSignatureAlgorithms("ECDSA"), 334 } 335 csrPEM, _, err := pkiutil.GenCSR(options) 336 if err != nil { 337 t.Fatalf("Error creating Mock CA client: %v", err) 338 return nil 339 } 340 return csrPEM 341 } 342 343 // newMockTLSServer creates a mock TLS server for testing purpose. 344 func newMockTLSServer(t *testing.T) *mockTLSServer { 345 server := &mockTLSServer{} 346 347 handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 348 t.Logf("request: %+v", *req) 349 switch req.URL.Path { 350 default: 351 t.Logf("The request contains path: %v", req.URL) 352 resp.WriteHeader(http.StatusOK) 353 } 354 }) 355 356 server.httpServer = httptest.NewTLSServer(handler) 357 358 t.Logf("Serving TLS at: %v", server.httpServer.URL) 359 360 return server 361 } 362 363 // Get the server port from server.URL (e.g., https://127.0.0.1:36253) 364 func getServerPort(server *httptest.Server) (int, error) { 365 strs := strings.Split(server.URL, ":") 366 if len(strs) < 2 { 367 return 0, fmt.Errorf("server.URL is invalid: %v", server.URL) 368 } 369 port, err := strconv.Atoi(strs[len(strs)-1]) 370 if err != nil { 371 return 0, fmt.Errorf("error to extract port from URL: %v", server.URL) 372 } 373 return port, nil 374 } 375 376 func initFakeKubeClient(t test.Failer, certificate []byte) kube.CLIClient { 377 client := kube.NewFakeClient() 378 ctx := test.NewContext(t) 379 w, _ := client.Kube().CertificatesV1().CertificateSigningRequests().Watch(ctx, metav1.ListOptions{}) 380 go func() { 381 for { 382 select { 383 case <-ctx.Done(): 384 return 385 case r := <-w.ResultChan(): 386 csr := r.Object.(*cert.CertificateSigningRequest).DeepCopy() 387 if csr.Status.Certificate != nil { 388 log.Debugf("test signer skip, already signed: %v", csr.Name) 389 continue 390 } 391 if approved(csr) { 392 // This is a pretty terrible hack, but client-go fake doesn't properly support list+watch, 393 // so any updates in between the list and watch would be missed. So give some time for the watch to start 394 time.Sleep(time.Millisecond * 25) 395 csr.Status.Certificate = certificate 396 _, err := client.Kube().CertificatesV1().CertificateSigningRequests().UpdateStatus(ctx, csr, metav1.UpdateOptions{}) 397 log.Debugf("test signer sign %v: %v", csr.Name, err) 398 } else { 399 log.Debugf("test signer skip, not approved: %v", csr.Name) 400 } 401 } 402 } 403 }() 404 return client 405 } 406 407 func approved(csr *cert.CertificateSigningRequest) bool { 408 return GetCondition(csr.Status.Conditions, cert.CertificateApproved).Status == corev1.ConditionTrue 409 } 410 411 func GetCondition(conditions []cert.CertificateSigningRequestCondition, condition cert.RequestConditionType) cert.CertificateSigningRequestCondition { 412 for _, cond := range conditions { 413 if cond.Type == condition { 414 return cond 415 } 416 } 417 return cert.CertificateSigningRequestCondition{} 418 }