k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/gencred/pkg/certificate/certificate.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package certificate 18 19 import ( 20 "context" 21 "crypto/ecdsa" 22 "crypto/elliptic" 23 "crypto/rand" 24 "crypto/x509" 25 "crypto/x509/pkix" 26 "encoding/pem" 27 "errors" 28 "fmt" 29 "time" 30 31 certificates "k8s.io/api/certificates/v1beta1" 32 corev1 "k8s.io/api/core/v1" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/util/wait" 35 "k8s.io/client-go/kubernetes" 36 clientcmdapi "k8s.io/client-go/tools/clientcmd/api/v1" 37 "k8s.io/client-go/util/cert" 38 "k8s.io/client-go/util/keyutil" 39 "k8s.io/test-infra/gencred/pkg/kubeconfig" 40 ) 41 42 const ( 43 // systemPrivilegedGroup is a superuser by default (i.e. bound to the cluster-admin ClusterRole). 44 systemPrivilegedGroup = "system:masters" 45 // waitInterval request poll interval. 46 waitInterval = time.Second 47 // waitTimeout request poll timeout. 48 waitTimeout = 20 * time.Second 49 ) 50 51 // generateCSR generates a certificate signing request (CSR). 52 func generateCSR() (*certificates.CertificateSigningRequest, []byte, error) { 53 54 // Generate a new private key. 55 pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 56 if err != nil { 57 return nil, nil, fmt.Errorf("generate key: %w", err) 58 } 59 60 // Marshal pk -> der. 61 der, err := x509.MarshalECPrivateKey(pk) 62 if err != nil { 63 return nil, nil, fmt.Errorf("marshal key to DER: %w", err) 64 } 65 66 // Generate PEM key. 67 keyPEM := pem.EncodeToMemory(&pem.Block{Type: keyutil.ECPrivateKeyBlockType, Bytes: der}) 68 69 // Generate a x509 certificate signing request. 70 csrPEM, err := cert.MakeCSR(pk, &pkix.Name{CommonName: "client", Organization: []string{systemPrivilegedGroup}}, nil, nil) 71 if err != nil { 72 return nil, nil, fmt.Errorf("create CSR from key: %w", err) 73 } 74 75 // Generate a Kubernetes CSR object. 76 csrObj := &certificates.CertificateSigningRequest{ 77 // Username, UID, Groups will be injected by API server. 78 ObjectMeta: metav1.ObjectMeta{ 79 Name: "", 80 GenerateName: "csr-", 81 }, 82 Spec: certificates.CertificateSigningRequestSpec{ 83 Request: csrPEM, 84 Usages: []certificates.KeyUsage{ 85 certificates.UsageDigitalSignature, 86 certificates.UsageKeyEncipherment, 87 certificates.UsageClientAuth, 88 }, 89 }, 90 } 91 92 return csrObj, keyPEM, nil 93 } 94 95 // requestCSR requests a certificate signing request (CSR). 96 func requestCSR(clientset kubernetes.Interface, csrObj *certificates.CertificateSigningRequest) ([]byte, error) { 97 client := clientset.CertificatesV1beta1().CertificateSigningRequests() 98 99 // Create CSR. 100 csrObj, err := client.Create(context.TODO(), csrObj, metav1.CreateOptions{}) 101 if err != nil { 102 return nil, fmt.Errorf("create CSR: %w", err) 103 } 104 105 csrName := csrObj.Name 106 appendApprovalCondition(csrObj) 107 108 // Approve CSR. 109 err = wait.Poll(waitInterval, waitTimeout, func() (bool, error) { 110 _, err = client.UpdateApproval(context.TODO(), csrObj, metav1.UpdateOptions{}) 111 if err != nil { 112 return false, err 113 } 114 115 return true, nil 116 }) 117 if err != nil { 118 return nil, fmt.Errorf("approve CSR: %w", err) 119 } 120 121 // Get CSR. 122 err = wait.Poll(waitInterval, waitTimeout, func() (bool, error) { 123 csrObj, err = client.Get(context.TODO(), csrName, metav1.GetOptions{}) 124 if err != nil { 125 return false, err 126 } 127 128 return true, nil 129 }) 130 if err != nil { 131 return nil, fmt.Errorf("get CSR: %w", err) 132 } 133 134 return csrObj.Status.Certificate, nil 135 } 136 137 // getRootCA fetches the service account root certificate authority (CA). 138 func getRootCA(clientset kubernetes.Interface) ([]byte, error) { 139 secrets, err := clientset.CoreV1().Secrets(metav1.NamespaceSystem).List(context.TODO(), metav1.ListOptions{}) 140 if err != nil { 141 return nil, err 142 } 143 if len(secrets.Items) == 0 { 144 return nil, errors.New("locate secrets") 145 } 146 147 caPEM, ok := secrets.Items[0].Data[corev1.ServiceAccountRootCAKey] 148 if !ok { 149 return nil, errors.New("locate root CA") 150 } 151 152 return caPEM, nil 153 } 154 155 // appendApprovalCondition appends the approval condition to the certificate signing request (CSR). 156 func appendApprovalCondition(csr *certificates.CertificateSigningRequest) { 157 csr.Status.Conditions = append(csr.Status.Conditions, certificates.CertificateSigningRequestCondition{ 158 Type: certificates.CertificateApproved, 159 Reason: "GenCredApprove", 160 Message: "This CSR was approved by gencred.", 161 LastUpdateTime: metav1.Now(), 162 }) 163 } 164 165 // CreateClusterCertificateCredentials creates a client certificate and key to authenticate to a cluster API server. 166 func CreateClusterCertificateCredentials(clientset kubernetes.Interface) (certPEM []byte, keyPEM []byte, caPEM []byte, err error) { 167 csrObj, keyPEM, err := generateCSR() 168 if err != nil { 169 return nil, nil, nil, fmt.Errorf("generate CSR: %w", err) 170 } 171 172 certPEM, err = requestCSR(clientset, csrObj) 173 if err != nil { 174 return nil, nil, nil, fmt.Errorf("request CSR: %w", err) 175 } 176 177 caPEM, err = getRootCA(clientset) 178 if err != nil { 179 return nil, nil, nil, fmt.Errorf("get root CA: %w", err) 180 } 181 182 return certPEM, keyPEM, caPEM, nil 183 } 184 185 // CreateKubeConfigWithCertificateCredentials creates a kube config containing a certificate and key to authenticate to a Kubernetes cluster API server. 186 func CreateKubeConfigWithCertificateCredentials(clientset kubernetes.Interface, name string) ([]byte, error) { 187 certPEM, keyPEM, caPEM, err := CreateClusterCertificateCredentials(clientset) 188 if err != nil { 189 return nil, err 190 } 191 192 authInfo := clientcmdapi.AuthInfo{ 193 ClientCertificateData: certPEM, 194 ClientKeyData: keyPEM, 195 } 196 197 return kubeconfig.CreateKubeConfig(clientset, name, caPEM, authInfo) 198 }