k8s.io/kubernetes@v1.29.3/test/integration/certificates/duration_test.go (about)

     1  /*
     2  Copyright 2021 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 certificates
    18  
    19  import (
    20  	"context"
    21  	"crypto/ecdsa"
    22  	"crypto/elliptic"
    23  	"crypto/rand"
    24  	"crypto/x509/pkix"
    25  	"encoding/pem"
    26  	"os"
    27  	"path"
    28  	"strings"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/google/go-cmp/cmp"
    33  
    34  	certificatesv1 "k8s.io/api/certificates/v1"
    35  	v1 "k8s.io/api/core/v1"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
    38  	"k8s.io/apimachinery/pkg/runtime/schema"
    39  	"k8s.io/apiserver/pkg/server/dynamiccertificates"
    40  	"k8s.io/client-go/informers"
    41  	clientset "k8s.io/client-go/kubernetes"
    42  	"k8s.io/client-go/rest"
    43  	certutil "k8s.io/client-go/util/cert"
    44  	"k8s.io/client-go/util/certificate/csr"
    45  	"k8s.io/client-go/util/keyutil"
    46  	"k8s.io/klog/v2/ktesting"
    47  	kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    48  	"k8s.io/kubernetes/pkg/controller/certificates/signer"
    49  	"k8s.io/kubernetes/test/integration/framework"
    50  	"k8s.io/utils/pointer"
    51  )
    52  
    53  func TestCSRDuration(t *testing.T) {
    54  	t.Parallel()
    55  
    56  	s := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd())
    57  	t.Cleanup(s.TearDownFn)
    58  
    59  	_, ctx := ktesting.NewTestContext(t)
    60  	ctx, cancel := context.WithTimeout(ctx, 3*time.Minute)
    61  	t.Cleanup(cancel)
    62  
    63  	// assert that the metrics we collect during the test run match expectations
    64  	// we have 7 valid test cases below that request a duration of which 6 should have their duration honored
    65  	wantMetricStrings := []string{
    66  		`apiserver_certificates_registry_csr_honored_duration_total{signerName="kubernetes.io/kube-apiserver-client"} 6`,
    67  		`apiserver_certificates_registry_csr_requested_duration_total{signerName="kubernetes.io/kube-apiserver-client"} 7`,
    68  	}
    69  	t.Cleanup(func() {
    70  		copyConfig := rest.CopyConfig(s.ClientConfig)
    71  		copyConfig.GroupVersion = &schema.GroupVersion{}
    72  		copyConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer()
    73  		rc, err := rest.RESTClientFor(copyConfig)
    74  		if err != nil {
    75  			t.Fatal(err)
    76  		}
    77  		body, err := rc.Get().AbsPath("/metrics").DoRaw(ctx)
    78  		if err != nil {
    79  			t.Fatal(err)
    80  		}
    81  		var gotMetricStrings []string
    82  		for _, line := range strings.Split(string(body), "\n") {
    83  			if strings.HasPrefix(line, "apiserver_certificates_registry_") {
    84  				gotMetricStrings = append(gotMetricStrings, line)
    85  			}
    86  		}
    87  		if diff := cmp.Diff(wantMetricStrings, gotMetricStrings); diff != "" {
    88  			t.Errorf("unexpected metrics diff (-want +got): %s", diff)
    89  		}
    90  	})
    91  
    92  	client := clientset.NewForConfigOrDie(s.ClientConfig)
    93  	informerFactory := informers.NewSharedInformerFactory(client, 0)
    94  
    95  	caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    96  	if err != nil {
    97  		t.Fatal(err)
    98  	}
    99  	caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey)
   100  	if err != nil {
   101  		t.Fatal(err)
   102  	}
   103  	caPublicKeyFile := path.Join(s.TmpDir, "test-ca-public-key")
   104  	if err := os.WriteFile(caPublicKeyFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}), os.FileMode(0600)); err != nil {
   105  		t.Fatal(err)
   106  	}
   107  	caPrivateKeyBytes, err := keyutil.MarshalPrivateKeyToPEM(caPrivateKey)
   108  	if err != nil {
   109  		t.Fatal(err)
   110  	}
   111  	caPrivateKeyFile := path.Join(s.TmpDir, "test-ca-private-key")
   112  	if err := os.WriteFile(caPrivateKeyFile, caPrivateKeyBytes, os.FileMode(0600)); err != nil {
   113  		t.Fatal(err)
   114  	}
   115  
   116  	c, err := signer.NewKubeAPIServerClientCSRSigningController(ctx, client, informerFactory.Certificates().V1().CertificateSigningRequests(), caPublicKeyFile, caPrivateKeyFile, 24*time.Hour)
   117  	if err != nil {
   118  		t.Fatal(err)
   119  	}
   120  
   121  	informerFactory.Start(ctx.Done())
   122  	go c.Run(ctx, 1)
   123  
   124  	tests := []struct {
   125  		name, csrName string
   126  		duration      *time.Duration
   127  		wantDuration  time.Duration
   128  		wantError     string
   129  	}{
   130  		{
   131  			name:         "no duration set",
   132  			duration:     nil,
   133  			wantDuration: 24 * time.Hour,
   134  			wantError:    "",
   135  		},
   136  		{
   137  			name:         "same duration set as certTTL",
   138  			duration:     pointer.Duration(24 * time.Hour),
   139  			wantDuration: 24 * time.Hour,
   140  			wantError:    "",
   141  		},
   142  		{
   143  			name:         "longer duration than certTTL",
   144  			duration:     pointer.Duration(48 * time.Hour),
   145  			wantDuration: 24 * time.Hour,
   146  			wantError:    "",
   147  		},
   148  		{
   149  			name:         "slightly shorter duration set",
   150  			duration:     pointer.Duration(20 * time.Hour),
   151  			wantDuration: 20 * time.Hour,
   152  			wantError:    "",
   153  		},
   154  		{
   155  			name:         "even shorter duration set",
   156  			duration:     pointer.Duration(10 * time.Hour),
   157  			wantDuration: 10 * time.Hour,
   158  			wantError:    "",
   159  		},
   160  		{
   161  			name:         "short duration set",
   162  			duration:     pointer.Duration(2 * time.Hour),
   163  			wantDuration: 2*time.Hour + 5*time.Minute,
   164  			wantError:    "",
   165  		},
   166  		{
   167  			name:         "very short duration set",
   168  			duration:     pointer.Duration(30 * time.Minute),
   169  			wantDuration: 30*time.Minute + 5*time.Minute,
   170  			wantError:    "",
   171  		},
   172  		{
   173  			name:         "shortest duration set",
   174  			duration:     pointer.Duration(10 * time.Minute),
   175  			wantDuration: 10*time.Minute + 5*time.Minute,
   176  			wantError:    "",
   177  		},
   178  		{
   179  			name:         "just too short duration set",
   180  			csrName:      "invalid-csr-001",
   181  			duration:     pointer.Duration(10*time.Minute - time.Second),
   182  			wantDuration: 0,
   183  			wantError: `cannot create certificate signing request: ` +
   184  				`CertificateSigningRequest.certificates.k8s.io "invalid-csr-001" is invalid: spec.expirationSeconds: Invalid value: 599: may not specify a duration less than 600 seconds (10 minutes)`,
   185  		},
   186  		{
   187  			name:         "really too short duration set",
   188  			csrName:      "invalid-csr-002",
   189  			duration:     pointer.Duration(3 * time.Minute),
   190  			wantDuration: 0,
   191  			wantError: `cannot create certificate signing request: ` +
   192  				`CertificateSigningRequest.certificates.k8s.io "invalid-csr-002" is invalid: spec.expirationSeconds: Invalid value: 180: may not specify a duration less than 600 seconds (10 minutes)`,
   193  		},
   194  		{
   195  			name:         "negative duration set",
   196  			csrName:      "invalid-csr-003",
   197  			duration:     pointer.Duration(-7 * time.Minute),
   198  			wantDuration: 0,
   199  			wantError: `cannot create certificate signing request: ` +
   200  				`CertificateSigningRequest.certificates.k8s.io "invalid-csr-003" is invalid: spec.expirationSeconds: Invalid value: -420: may not specify a duration less than 600 seconds (10 minutes)`,
   201  		},
   202  	}
   203  	for _, tt := range tests {
   204  		tt := tt
   205  		t.Run(tt.name, func(t *testing.T) {
   206  			t.Parallel()
   207  
   208  			privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   209  			if err != nil {
   210  				t.Fatal(err)
   211  			}
   212  			csrData, err := certutil.MakeCSR(privateKey, &pkix.Name{CommonName: "panda"}, nil, nil)
   213  			if err != nil {
   214  				t.Fatal(err)
   215  			}
   216  
   217  			csrName, csrUID, errReq := csr.RequestCertificate(client, csrData, tt.csrName, certificatesv1.KubeAPIServerClientSignerName,
   218  				tt.duration, []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth}, privateKey)
   219  
   220  			if diff := cmp.Diff(tt.wantError, errStr(errReq)); len(diff) > 0 {
   221  				t.Fatalf("CSR input duration %v err diff (-want, +got):\n%s", tt.duration, diff)
   222  			}
   223  
   224  			if len(tt.wantError) > 0 {
   225  				return
   226  			}
   227  
   228  			csrObj, err := client.CertificatesV1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{})
   229  			if err != nil {
   230  				t.Fatal(err)
   231  			}
   232  			csrObj.Status.Conditions = []certificatesv1.CertificateSigningRequestCondition{
   233  				{
   234  					Type:    certificatesv1.CertificateApproved,
   235  					Status:  v1.ConditionTrue,
   236  					Reason:  "TestCSRDuration",
   237  					Message: t.Name(),
   238  				},
   239  			}
   240  			_, err = client.CertificatesV1().CertificateSigningRequests().UpdateApproval(ctx, csrName, csrObj, metav1.UpdateOptions{})
   241  			if err != nil {
   242  				t.Fatal(err)
   243  			}
   244  
   245  			certData, err := csr.WaitForCertificate(ctx, client, csrName, csrUID)
   246  			if err != nil {
   247  				t.Fatal(err)
   248  			}
   249  
   250  			certs, err := certutil.ParseCertsPEM(certData)
   251  			if err != nil {
   252  				t.Fatal(err)
   253  			}
   254  
   255  			switch l := len(certs); l {
   256  			case 1:
   257  				// good
   258  			default:
   259  				t.Errorf("expected 1 cert, got %d", l)
   260  				for i, certificate := range certs {
   261  					t.Log(i, dynamiccertificates.GetHumanCertDetail(certificate))
   262  				}
   263  				t.FailNow()
   264  			}
   265  
   266  			cert := certs[0]
   267  
   268  			if got := cert.NotAfter.Sub(cert.NotBefore); got != tt.wantDuration {
   269  				t.Errorf("CSR input duration %v got duration = %v, want %v\n%s", tt.duration, got, tt.wantDuration, dynamiccertificates.GetHumanCertDetail(cert))
   270  			}
   271  		})
   272  	}
   273  }
   274  
   275  func errStr(err error) string {
   276  	if err == nil {
   277  		return ""
   278  	}
   279  	es := err.Error()
   280  	if len(es) == 0 {
   281  		panic("invalid empty error")
   282  	}
   283  	return es
   284  }