diff --git a/pkg/server/options/client-go-certutil-copy.go b/pkg/server/options/client-go-certutil-copy.go new file mode 100644 index 000000000..154bdb59c --- /dev/null +++ b/pkg/server/options/client-go-certutil-copy.go @@ -0,0 +1,205 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// NOTE: As part of the work to resolve https://issues.redhat.com/browse/OCPBUGS-61760 +// and https://issues.redhat.com/browse/OCPBUGS-61759 it was decided that copying +// the loopback certification creation utility function to a local +// utility function would save significant effort over cherry-picking +// the fix from https://github.com/kubernetes/kubernetes/pull/130047 to +// https://github.com/openshift/kubernetes-client-go . +// We don't expect to backport many changes on top of this and it seemed +// lower risk than switching all of our aggregated API server to our fork of +// client-go because almost nothing currently depends on it. +// +// The only difference we expect to be present when comparing the +// `GenerateSelfSignedCertKeyWithFixtures` function in this file +// with the one present in the v0.31.1 client-go source[1] is +// the modification to change the maxAge variable to ~3 years. +// +// [1]: https://github.com/kubernetes/client-go/blob/c5196ebcc18e1bf29561b7a689fdb121913d221b/util/cert/cert.go#L100-L222 + +package options + +import ( + "bytes" + cryptorand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math" + "math/big" + "net" + "os" + "path/filepath" + "strings" + "time" + + "k8s.io/client-go/util/keyutil" + netutils "k8s.io/utils/net" +) + +// GenerateSelfSignedCertKey creates a self-signed certificate and key for the given host. +// Host may be an IP or a DNS name +// You may also specify additional subject alt names (either ip or dns names) for the certificate. +func GenerateSelfSignedCertKey(host string, alternateIPs []net.IP, alternateDNS []string) ([]byte, []byte, error) { + return GenerateSelfSignedCertKeyWithFixtures(host, alternateIPs, alternateDNS, "") +} + +// GenerateSelfSignedCertKeyWithFixtures creates a self-signed certificate and key for the given host. +// Host may be an IP or a DNS name. You may also specify additional subject alt names (either ip or dns names) +// for the certificate. +// +// If fixtureDirectory is non-empty, it is a directory path which can contain pre-generated certs. The format is: +// _-_-.crt +// _-_-.key +// Certs/keys not existing in that directory are created. +func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, alternateDNS []string, fixtureDirectory string) ([]byte, []byte, error) { + validFrom := time.Now().Add(-time.Hour) // valid an hour earlier to avoid flakes due to clock skew + + // NOTE: Modified from the original of 1 year to 3 years to fix + // - https://issues.redhat.com/browse/OCPBUGS-61760 + // - https://issues.redhat.com/browse/OCPBUGS-61759 + // Achieves the same result as the upstream change in https://github.com/kubernetes/kubernetes/pull/130047 + maxAge := time.Hour * 24 * (3*365 + 1) // three year self-signed certs + + baseName := fmt.Sprintf("%s_%s_%s", host, strings.Join(ipsToStrings(alternateIPs), "-"), strings.Join(alternateDNS, "-")) + certFixturePath := filepath.Join(fixtureDirectory, baseName+".crt") + keyFixturePath := filepath.Join(fixtureDirectory, baseName+".key") + if len(fixtureDirectory) > 0 { + cert, err := os.ReadFile(certFixturePath) + if err == nil { + key, err := os.ReadFile(keyFixturePath) + if err == nil { + return cert, key, nil + } + return nil, nil, fmt.Errorf("cert %s can be read, but key %s cannot: %v", certFixturePath, keyFixturePath, err) + } + maxAge = 100 * time.Hour * 24 * 365 // 100 years fixtures + } + + caKey, err := rsa.GenerateKey(cryptorand.Reader, 2048) + if err != nil { + return nil, nil, err + } + // returns a uniform random value in [0, max-1), then add 1 to serial to make it a uniform random value in [1, max). + serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64-1)) + if err != nil { + return nil, nil, err + } + serial = new(big.Int).Add(serial, big.NewInt(1)) + caTemplate := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: fmt.Sprintf("%s-ca@%d", host, time.Now().Unix()), + }, + NotBefore: validFrom, + NotAfter: validFrom.Add(maxAge), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + } + + caDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + caCertificate, err := x509.ParseCertificate(caDERBytes) + if err != nil { + return nil, nil, err + } + + priv, err := rsa.GenerateKey(cryptorand.Reader, 2048) + if err != nil { + return nil, nil, err + } + // returns a uniform random value in [0, max-1), then add 1 to serial to make it a uniform random value in [1, max). + serial, err = cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64-1)) + if err != nil { + return nil, nil, err + } + serial = new(big.Int).Add(serial, big.NewInt(1)) + template := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()), + }, + NotBefore: validFrom, + NotAfter: validFrom.Add(maxAge), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + if ip := netutils.ParseIPSloppy(host); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, host) + } + + template.IPAddresses = append(template.IPAddresses, alternateIPs...) + template.DNSNames = append(template.DNSNames, alternateDNS...) + + derBytes, err := x509.CreateCertificate(cryptorand.Reader, &template, caCertificate, &priv.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + // Generate cert, followed by ca + certBuffer := bytes.Buffer{} + if err := pem.Encode(&certBuffer, &pem.Block{Type: CertificateBlockType, Bytes: derBytes}); err != nil { + return nil, nil, err + } + if err := pem.Encode(&certBuffer, &pem.Block{Type: CertificateBlockType, Bytes: caDERBytes}); err != nil { + return nil, nil, err + } + + // Generate key + keyBuffer := bytes.Buffer{} + if err := pem.Encode(&keyBuffer, &pem.Block{Type: keyutil.RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { + return nil, nil, err + } + + if len(fixtureDirectory) > 0 { + if err := os.WriteFile(certFixturePath, certBuffer.Bytes(), 0644); err != nil { + return nil, nil, fmt.Errorf("failed to write cert fixture to %s: %v", certFixturePath, err) + } + if err := os.WriteFile(keyFixturePath, keyBuffer.Bytes(), 0600); err != nil { + return nil, nil, fmt.Errorf("failed to write key fixture to %s: %v", certFixturePath, err) + } + } + + return certBuffer.Bytes(), keyBuffer.Bytes(), nil +} + +func ipsToStrings(ips []net.IP) []string { + ss := make([]string, 0, len(ips)) + for _, ip := range ips { + ss = append(ss, ip.String()) + } + return ss +} + +const ( + // CertificateBlockType is a possible value for pem.Block.Type. + CertificateBlockType = "CERTIFICATE" + // CertificateRequestBlockType is a possible value for pem.Block.Type. + CertificateRequestBlockType = "CERTIFICATE REQUEST" +) diff --git a/pkg/server/options/serving_with_loopback.go b/pkg/server/options/serving_with_loopback.go index 2317be82d..b18a750ba 100644 --- a/pkg/server/options/serving_with_loopback.go +++ b/pkg/server/options/serving_with_loopback.go @@ -24,7 +24,6 @@ import ( "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/client-go/rest" - certutil "k8s.io/client-go/util/cert" ) type SecureServingOptionsWithLoopback struct { @@ -51,7 +50,7 @@ func (s *SecureServingOptionsWithLoopback) ApplyTo(secureServingInfo **server.Se // create self-signed cert+key with the fake server.LoopbackClientServerNameOverride and // let the server return it when the loopback client connects. - certPem, keyPem, err := certutil.GenerateSelfSignedCertKey(server.LoopbackClientServerNameOverride, nil, nil) + certPem, keyPem, err := GenerateSelfSignedCertKey(server.LoopbackClientServerNameOverride, nil, nil) // use forked GenererateSelfSignedCertKey with extended expiry if err != nil { return fmt.Errorf("failed to generate self-signed certificate for loopback connection: %v", err) }