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  }