istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/k8s/chiron/utils.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 "context" 19 "crypto/x509" 20 "encoding/pem" 21 "fmt" 22 "net" 23 "os" 24 "time" 25 26 cert "k8s.io/api/certificates/v1" 27 corev1 "k8s.io/api/core/v1" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/fields" 30 clientset "k8s.io/client-go/kubernetes" 31 32 "istio.io/istio/pkg/log" 33 "istio.io/istio/pkg/ptr" 34 "istio.io/istio/security/pkg/pki/util" 35 ) 36 37 const ( 38 // The size of a private key for a leaf certificate. 39 keySize = 2048 40 ) 41 42 var certWatchTimeout = 60 * time.Second 43 44 // GenKeyCertK8sCA : Generates a key pair and gets public certificate signed by K8s_CA 45 // Options are meant to sign DNS certs 46 // 1. Generate a CSR 47 // 2. Call SignCSRK8s to finish rest of the flow 48 func GenKeyCertK8sCA(client clientset.Interface, dnsName, 49 caFilePath string, signerName string, approveCsr bool, requestedLifetime time.Duration, 50 ) ([]byte, []byte, []byte, error) { 51 // 1. Generate a CSR 52 options := util.CertOptions{ 53 Host: dnsName, 54 RSAKeySize: keySize, 55 IsDualUse: false, 56 PKCS8Key: false, 57 } 58 csrPEM, keyPEM, err := util.GenCSR(options) 59 if err != nil { 60 log.Errorf("CSR generation error (%v)", err) 61 return nil, nil, nil, err 62 } 63 usages := []cert.KeyUsage{ 64 cert.UsageDigitalSignature, 65 cert.UsageKeyEncipherment, 66 cert.UsageServerAuth, 67 } 68 if signerName == "" { 69 return nil, nil, nil, fmt.Errorf("signerName is required for Kubernetes CA") 70 } 71 certChain, caCert, err := SignCSRK8s(client, csrPEM, signerName, usages, dnsName, caFilePath, approveCsr, true, requestedLifetime) 72 73 return certChain, keyPEM, caCert, err 74 } 75 76 // SignCSRK8s generates a certificate from CSR using the K8s CA 77 // 1. Submit a CSR 78 // 2. Approve a CSR 79 // 3. Read the signed certificate 80 // 4. Clean up the artifacts (e.g., delete CSR) 81 func SignCSRK8s(client clientset.Interface, csrData []byte, signerName string, usages []cert.KeyUsage, 82 dnsName, caFilePath string, approveCsr, appendCaCert bool, requestedLifetime time.Duration, 83 ) ([]byte, []byte, error) { 84 // 1. Submit the CSR 85 csr, err := submitCSR(client, csrData, signerName, usages, requestedLifetime) 86 if err != nil { 87 return nil, nil, err 88 } 89 log.Debugf("CSR (%v) has been created", csr.Name) 90 91 // clean up certificate request after deletion 92 defer func() { 93 _ = cleanupCSR(client, csr) 94 }() 95 96 // 2. Approve the CSR 97 if approveCsr { 98 approvalMessage := fmt.Sprintf("CSR (%s) for the certificate (%s) is approved", csr.Name, dnsName) 99 err = approveCSR(client, csr, approvalMessage) 100 if err != nil { 101 return nil, nil, fmt.Errorf("failed to approve CSR request: %v", err) 102 } 103 log.Debugf("CSR (%v) is approved", csr.Name) 104 } 105 106 // 3. Read the signed certificate 107 certChain, caCert, err := readSignedCertificate(client, csr, certWatchTimeout, caFilePath, appendCaCert) 108 if err != nil { 109 return nil, nil, err 110 } 111 112 // If there is a failure of cleaning up CSR, the error is returned. 113 return certChain, caCert, err 114 } 115 116 // Read CA certificate and check whether it is a valid certificate. 117 func readCACert(caCertPath string) ([]byte, error) { 118 caCert, err := os.ReadFile(caCertPath) 119 if err != nil { 120 log.Errorf("failed to read CA cert, cert. path: %v, error: %v", caCertPath, err) 121 return nil, fmt.Errorf("failed to read CA cert, cert. path: %v, error: %v", caCertPath, err) 122 } 123 124 b, _ := pem.Decode(caCert) 125 if b == nil { 126 return nil, fmt.Errorf("could not decode pem") 127 } 128 if b.Type != "CERTIFICATE" { 129 return nil, fmt.Errorf("ca certificate contains wrong type: %v", b.Type) 130 } 131 if _, err := x509.ParseCertificate(b.Bytes); err != nil { 132 return nil, fmt.Errorf("ca certificate parsing returns an error: %v", err) 133 } 134 135 return caCert, nil 136 } 137 138 func isTCPReachable(host string, port int) bool { 139 addr := fmt.Sprintf("%s:%d", host, port) 140 conn, err := net.DialTimeout("tcp", addr, 1*time.Second) 141 if err != nil { 142 log.Debugf("DialTimeout() returns err: %v", err) 143 // No connection yet, so no need to conn.Close() 144 return false 145 } 146 err = conn.Close() 147 if err != nil { 148 log.Infof("tcp connection is not closed: %v", err) 149 } 150 return true 151 } 152 153 func submitCSR( 154 client clientset.Interface, 155 csrData []byte, 156 signerName string, 157 usages []cert.KeyUsage, 158 requestedLifetime time.Duration, 159 ) (*cert.CertificateSigningRequest, error) { 160 log.Debugf("create CSR for signer %v", signerName) 161 csr := &cert.CertificateSigningRequest{ 162 // Username, UID, Groups will be injected by API server. 163 TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"}, 164 ObjectMeta: metav1.ObjectMeta{ 165 GenerateName: "csr-workload-", 166 }, 167 Spec: cert.CertificateSigningRequestSpec{ 168 Request: csrData, 169 Usages: usages, 170 SignerName: signerName, 171 }, 172 } 173 if requestedLifetime != time.Duration(0) { 174 csr.Spec.ExpirationSeconds = ptr.Of(int32(requestedLifetime.Seconds())) 175 } 176 resp, err := client.CertificatesV1().CertificateSigningRequests().Create(context.Background(), csr, metav1.CreateOptions{}) 177 if err != nil { 178 return nil, fmt.Errorf("failed to create CSR: %v", err) 179 } 180 return resp, nil 181 } 182 183 func approveCSR(client clientset.Interface, csr *cert.CertificateSigningRequest, approvalMessage string) error { 184 csr.Status.Conditions = append(csr.Status.Conditions, cert.CertificateSigningRequestCondition{ 185 Type: cert.CertificateApproved, 186 Reason: approvalMessage, 187 Message: approvalMessage, 188 Status: corev1.ConditionTrue, 189 }) 190 _, err := client.CertificatesV1().CertificateSigningRequests().UpdateApproval(context.TODO(), csr.Name, csr, metav1.UpdateOptions{}) 191 if err != nil { 192 log.Errorf("failed to approve CSR (%v): %v", csr.Name, err) 193 return err 194 } 195 return nil 196 } 197 198 // Read the signed certificate 199 // verify and append CA certificate to certChain if appendCaCert is true 200 func readSignedCertificate(client clientset.Interface, csr *cert.CertificateSigningRequest, 201 watchTimeout time.Duration, caCertPath string, appendCaCert bool, 202 ) ([]byte, []byte, error) { 203 // First try to read the signed CSR through a watching mechanism 204 certPEM, err := readSignedCsr(client, csr.Name, watchTimeout) 205 if err != nil { 206 return nil, nil, err 207 } 208 if len(certPEM) == 0 { 209 return nil, nil, fmt.Errorf("no certificate returned for the CSR: %q", csr.Name) 210 } 211 certsParsed, _, err := util.ParsePemEncodedCertificateChain(certPEM) 212 if err != nil { 213 return nil, nil, fmt.Errorf("decoding certificate failed") 214 } 215 if !appendCaCert || caCertPath == "" { 216 return certPEM, nil, nil 217 } 218 caCert, err := readCACert(caCertPath) 219 if err != nil { 220 return nil, nil, fmt.Errorf("error when retrieving CA cert: (%v)", err) 221 } 222 // Verify the certificate chain before returning the certificate 223 roots := x509.NewCertPool() 224 if roots == nil { 225 return nil, nil, fmt.Errorf("failed to create cert pool") 226 } 227 if ok := roots.AppendCertsFromPEM(caCert); !ok { 228 return nil, nil, fmt.Errorf("failed to append CA certificate") 229 } 230 intermediates := x509.NewCertPool() 231 if len(certsParsed) > 1 { 232 for _, cert := range certsParsed[1:] { 233 intermediates.AddCert(cert) 234 } 235 } 236 _, err = certsParsed[0].Verify(x509.VerifyOptions{ 237 Roots: roots, 238 Intermediates: intermediates, 239 }) 240 if err != nil { 241 return nil, nil, fmt.Errorf("failed to verify the certificate chain: %v", err) 242 } 243 244 return append(certPEM, caCert...), caCert, nil 245 } 246 247 // Return signed CSR through a watcher. If no CSR is read, return nil. 248 func readSignedCsr(client clientset.Interface, csr string, watchTimeout time.Duration) ([]byte, error) { 249 selector := fields.OneTermEqualSelector("metadata.name", csr).String() 250 // Setup a List+Watch, like informers do 251 // A simple Watch will fail if the cert is signed too quickly 252 l, _ := client.CertificatesV1().CertificateSigningRequests().List(context.Background(), metav1.ListOptions{ 253 FieldSelector: selector, 254 }) 255 if l != nil && len(l.Items) > 0 { 256 reqSigned := l.Items[0] 257 if reqSigned.Status.Certificate != nil { 258 return reqSigned.Status.Certificate, nil 259 } 260 } 261 var rv string 262 if l != nil { 263 rv = l.ResourceVersion 264 } 265 watcher, err := client.CertificatesV1().CertificateSigningRequests().Watch(context.Background(), metav1.ListOptions{ 266 ResourceVersion: rv, 267 FieldSelector: selector, 268 }) 269 if err != nil { 270 return nil, fmt.Errorf("failed to watch CSR %v", csr) 271 } 272 273 // Set a timeout 274 timer := time.After(watchTimeout) 275 for { 276 select { 277 case r := <-watcher.ResultChan(): 278 reqSigned := r.Object.(*cert.CertificateSigningRequest) 279 if reqSigned.Status.Certificate != nil { 280 return reqSigned.Status.Certificate, nil 281 } 282 case <-timer: 283 return nil, fmt.Errorf("timeout when watching CSR %v", csr) 284 } 285 } 286 } 287 288 // Clean up the CSR 289 func cleanupCSR(client clientset.Interface, csr *cert.CertificateSigningRequest) error { 290 err := client.CertificatesV1().CertificateSigningRequests().Delete(context.TODO(), csr.Name, metav1.DeleteOptions{}) 291 if err != nil { 292 log.Errorf("failed to delete CSR (%v): %v", csr.Name, err) 293 } else { 294 log.Debugf("deleted CSR: %v", csr.Name) 295 } 296 return err 297 }