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 }