diff --git a/go.mod b/go.mod index 12ea627c25..8644e00b52 100644 --- a/go.mod +++ b/go.mod @@ -102,9 +102,10 @@ require ( require ( github.com/openshift/api v0.0.0-20260320151444-324a1bcb9f55 github.com/openshift/client-go v0.0.0-20260320040014-4b5fc2cdad98 - github.com/openshift/library-go v0.0.0-20260303171201-5d9eb6295ff6 + github.com/openshift/library-go v0.0.0-20260409165127-c57da2bf5720 github.com/openshift/machine-config-operator v0.0.1-0.20250724162154-ab14c8e2843b k8s.io/apiextensions-apiserver v0.35.2 + k8s.io/apiserver v0.35.2 k8s.io/client-go v0.35.2 sigs.k8s.io/controller-tools v0.20.1 ) @@ -154,7 +155,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect - k8s.io/apiserver v0.35.2 // indirect k8s.io/gengo/v2 v2.0.0-20251215205346-5ee0d033ba5b // indirect k8s.io/kms v0.35.2 // indirect k8s.io/kube-aggregator v0.35.1 // indirect diff --git a/go.sum b/go.sum index e0875a468f..ae7a003c82 100644 --- a/go.sum +++ b/go.sum @@ -215,8 +215,8 @@ github.com/openshift/build-machinery-go v0.0.0-20251023084048-5d77c1a5e5af h1:Ui github.com/openshift/build-machinery-go v0.0.0-20251023084048-5d77c1a5e5af/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20260320040014-4b5fc2cdad98 h1:Ssuo/zELWqb7pFCwzB3QGEA4QeLW948hL2AhWq2SWjs= github.com/openshift/client-go v0.0.0-20260320040014-4b5fc2cdad98/go.mod h1:8O4jIKdcr5YR9FFQEeokYoCplCUN+j9hZj4u/2yg0As= -github.com/openshift/library-go v0.0.0-20260303171201-5d9eb6295ff6 h1:xjqy0OolrFdJ+ofI/aD0+2k9+MSk5anP5dXifFt539Q= -github.com/openshift/library-go v0.0.0-20260303171201-5d9eb6295ff6/go.mod h1:D797O/ssKTNglbrGchjIguFq+DbyRYdeds5w4/VTrKM= +github.com/openshift/library-go v0.0.0-20260409165127-c57da2bf5720 h1:ljxETzJc/vDgMJlaSB8GRryBHZOAIENA2Wo5arD8avM= +github.com/openshift/library-go v0.0.0-20260409165127-c57da2bf5720/go.mod h1:3bi4pLpYRdVd1aEhsHfRTJkwxwPLfRZ+ZePn3RmJd2k= github.com/openshift/machine-config-operator v0.0.1-0.20250724162154-ab14c8e2843b h1:LvoFr/2IEj0BWy7mKBdR7ueAHpMJGju1EkEIZrXa+DM= github.com/openshift/machine-config-operator v0.0.1-0.20250724162154-ab14c8e2843b/go.mod h1:UL1OVkRAUkB4aaFZrLlSvuY0jayfdF+o+ZxKiKaaArc= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= diff --git a/pkg/controller/add_networkconfig.go b/pkg/controller/add_networkconfig.go index 64c8b2399b..234eb47036 100644 --- a/pkg/controller/add_networkconfig.go +++ b/pkg/controller/add_networkconfig.go @@ -9,15 +9,14 @@ import ( "github.com/openshift/cluster-network-operator/pkg/controller/infrastructureconfig" "github.com/openshift/cluster-network-operator/pkg/controller/ingressconfig" "github.com/openshift/cluster-network-operator/pkg/controller/operconfig" - "github.com/openshift/cluster-network-operator/pkg/controller/pki" "github.com/openshift/cluster-network-operator/pkg/controller/proxyconfig" signer "github.com/openshift/cluster-network-operator/pkg/controller/signer" ) func init() { // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. + // Note: pki.Add is called separately in operator.go with additional parameters. AddToManagerFuncs = append(AddToManagerFuncs, - pki.Add, egress_router.Add, proxyconfig.Add, operconfig.Add, diff --git a/pkg/controller/pki/pki_controller.go b/pkg/controller/pki/pki_controller.go index bb0afa7577..eb78e2f958 100644 --- a/pkg/controller/pki/pki_controller.go +++ b/pkg/controller/pki/pki_controller.go @@ -8,7 +8,6 @@ package pki import ( "context" - "crypto/x509" "fmt" "log" "reflect" @@ -16,21 +15,21 @@ import ( "time" netopv1 "github.com/openshift/cluster-network-operator/pkg/apis/network/v1" - cnoclient "github.com/openshift/cluster-network-operator/pkg/client" "github.com/openshift/cluster-network-operator/pkg/controller/eventrecorder" "github.com/openshift/cluster-network-operator/pkg/controller/statusmanager" "github.com/openshift/cluster-network-operator/pkg/names" + features "github.com/openshift/api/features" "github.com/openshift/library-go/pkg/controller/factory" - "github.com/openshift/library-go/pkg/crypto" "github.com/openshift/library-go/pkg/operator/certrotation" + "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + "github.com/openshift/library-go/pkg/pki" "github.com/pkg/errors" - features "github.com/openshift/api/features" - "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/authentication/user" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" crclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -46,8 +45,8 @@ const ( ) // Add attaches our control loop to the manager and watches for PKI objects -func Add(mgr manager.Manager, status *statusmanager.StatusManager, _ cnoclient.Client, featureGates featuregates.FeatureGate) error { - r, err := newPKIReconciler(mgr, status, featureGates) +func Add(mgr manager.Manager, status *statusmanager.StatusManager, featureGates featuregates.FeatureGate, pkiProfileProvider pki.PKIProfileProvider) error { + r, err := newPKIReconciler(mgr, status, featureGates, pkiProfileProvider) if err != nil { return err } @@ -76,11 +75,15 @@ type PKIReconciler struct { status *statusmanager.StatusManager // one PKI per CA - pkis map[types.NamespacedName]*pki + pkis map[types.NamespacedName]*operatorPKI // For computing status pkiErrs map[types.NamespacedName]error certDuration time.Duration + + // pkiProfileProvider is non-nil when the ConfigurablePKI feature gate is + // enabled. It provides the PKI profile for certificate key configuration. + pkiProfileProvider pki.PKIProfileProvider } // The periodic resync interval. @@ -90,7 +93,7 @@ var ResyncPeriod = 5 * time.Minute // newPKIReconciler creates the toplevel reconciler that receives PKI updates // and configures the CertRotationController accordingly -func newPKIReconciler(mgr manager.Manager, status *statusmanager.StatusManager, featureGates featuregates.FeatureGate) (reconcile.Reconciler, error) { +func newPKIReconciler(mgr manager.Manager, status *statusmanager.StatusManager, featureGates featuregates.FeatureGate, pkiProfileProvider pki.PKIProfileProvider) (reconcile.Reconciler, error) { clientset, err := kubernetes.NewForConfig(mgr.GetConfig()) if err != nil { return nil, err @@ -106,9 +109,10 @@ func newPKIReconciler(mgr manager.Manager, status *statusmanager.StatusManager, status: status, clientset: clientset, - pkis: map[types.NamespacedName]*pki{}, - pkiErrs: map[types.NamespacedName]error{}, - certDuration: certDuration, + pkis: map[types.NamespacedName]*operatorPKI{}, + pkiErrs: map[types.NamespacedName]error{}, + certDuration: certDuration, + pkiProfileProvider: pkiProfileProvider, }, nil } @@ -139,7 +143,7 @@ func (r *PKIReconciler) Reconcile(ctx context.Context, request reconcile.Request } } if existing == nil { - existing, err = newPKI(obj, r.clientset, r.mgr, r.certDuration) + existing, err = newPKI(obj, r.clientset, r.mgr, r.certDuration, r.pkiProfileProvider) if err != nil { log.Println(err) r.pkiErrs[request.NamespacedName] = @@ -179,15 +183,15 @@ func (r *PKIReconciler) setStatus() { } } -// pki is the internal type that represents a single PKI CRD. It manages the +// operatorPKI is the internal type that represents a single PKI CRD. It manages the // business of reconciling the certificate objects -type pki struct { +type operatorPKI struct { spec netopv1.OperatorPKISpec controller factory.Controller } // newPKI creates a CertRotationController for the supplied configuration -func newPKI(config *netopv1.OperatorPKI, clientset *kubernetes.Clientset, mgr manager.Manager, certDuration time.Duration) (*pki, error) { +func newPKI(config *netopv1.OperatorPKI, clientset *kubernetes.Clientset, mgr manager.Manager, certDuration time.Duration, pkiProfileProvider pki.PKIProfileProvider) (*operatorPKI, error) { spec := config.Spec // Ugly: the existing cache + informers used as part of the controller-manager @@ -209,12 +213,14 @@ func newPKI(config *netopv1.OperatorPKI, clientset *kubernetes.Clientset, mgr ma AdditionalAnnotations: certrotation.AdditionalAnnotations{ JiraComponent: names.ClusterNetworkOperatorJiraComponent, }, - Validity: 10 * OneYear, - Refresh: 9 * OneYear, - Informer: inf.Core().V1().Secrets(), - Lister: inf.Core().V1().Secrets().Lister(), - Client: clientset.CoreV1(), - EventRecorder: &eventrecorder.LoggingRecorder{}, + CertificateName: "network.signer", + PKIProfileProvider: pkiProfileProvider, + Validity: 10 * OneYear, + Refresh: 9 * OneYear, + Informer: inf.Core().V1().Secrets(), + Lister: inf.Core().V1().Secrets().Lister(), + Client: clientset.CoreV1(), + EventRecorder: &eventrecorder.LoggingRecorder{}, }, certrotation.CABundleConfigMap{ Namespace: config.Namespace, @@ -233,15 +239,13 @@ func newPKI(config *netopv1.OperatorPKI, clientset *kubernetes.Clientset, mgr ma AdditionalAnnotations: certrotation.AdditionalAnnotations{ JiraComponent: names.ClusterNetworkOperatorJiraComponent, }, - Validity: certDuration, - Refresh: certDuration / 2, - CertCreator: &certrotation.ServingRotation{ + CertificateName: "network.peer", + PKIProfileProvider: pkiProfileProvider, + Validity: certDuration, + Refresh: certDuration / 2, + CertCreator: &certrotation.PeerRotation{ Hostnames: func() []string { return []string{spec.TargetCert.CommonName} }, - - // Force the certificate to also be client - CertificateExtensionFn: []crypto.CertificateExtensionFunc{ - toClientCert, - }, + UserInfo: &user.DefaultInfo{Name: spec.TargetCert.CommonName}, }, Lister: inf.Core().V1().Secrets().Lister(), Informer: inf.Core().V1().Secrets(), @@ -252,7 +256,7 @@ func newPKI(config *netopv1.OperatorPKI, clientset *kubernetes.Clientset, mgr ma nil, ) - out := &pki{ + out := &operatorPKI{ controller: cont, } config.Spec.DeepCopyInto(&out.spec) @@ -265,29 +269,7 @@ func newPKI(config *netopv1.OperatorPKI, clientset *kubernetes.Clientset, mgr ma } // sync causes the underlying cert controller to try and reconcile -func (p *pki) sync() error { +func (p *operatorPKI) sync() error { runOnceCtx := context.WithValue(context.Background(), certrotation.RunOnceContextKey, true) //nolint:staticcheck return p.controller.Sync(runOnceCtx, nil) } - -// toClientCert is a certificate "decorator" that adds ClientAuth to the -// list of ExtendedKeyUsages. This allows the generated certificate to be -// used for both client and server auth. -func toClientCert(cert *x509.Certificate) error { - if len(cert.ExtKeyUsage) == 0 { - return nil - } - - found := false - for _, u := range cert.ExtKeyUsage { - if u == x509.ExtKeyUsageClientAuth { - found = true - break - } - } - - if !found { - cert.ExtKeyUsage = append(cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth) - } - return nil -} diff --git a/pkg/controller/signer/signer.go b/pkg/controller/signer/signer.go index fd37ebb9c1..4c946e1ea1 100644 --- a/pkg/controller/signer/signer.go +++ b/pkg/controller/signer/signer.go @@ -3,7 +3,6 @@ package signer import ( c "crypto" "crypto/rand" - "crypto/rsa" "crypto/x509" "encoding/pem" "errors" @@ -21,8 +20,6 @@ func newCertificateTemplate(certReq *x509.CertificateRequest, certDuration time. template := &x509.Certificate{ Subject: certReq.Subject, - SignatureAlgorithm: x509.SHA512WithRSA, - NotBefore: time.Now().Add(-1 * time.Second), NotAfter: time.Now().Add(certDuration), SerialNumber: big.NewInt(serialNumber), @@ -69,13 +66,28 @@ func decodeCertificate(pemBytes []byte) (*x509.Certificate, error) { return x509.ParseCertificate(block.Bytes) } -func decodePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) { +func decodePrivateKey(pemBytes []byte) (c.Signer, error) { block, _ := pem.Decode(pemBytes) - if block == nil || block.Type != "RSA PRIVATE KEY" { - fmt.Println(block.Type) - err := errors.New("PEM block type must be RSA PRIVATE KEY") - return nil, err + if block == nil { + return nil, errors.New("no PEM block found in private key data") } - return x509.ParsePKCS1PrivateKey(block.Bytes) + switch block.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(block.Bytes) + case "PRIVATE KEY": + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse PKCS8 private key: %w", err) + } + signer, ok := key.(c.Signer) + if !ok { + return nil, fmt.Errorf("parsed private key does not implement crypto.Signer") + } + return signer, nil + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(block.Bytes) + default: + return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type) + } } diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 7351a75d3b..7d5d976095 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -8,6 +8,7 @@ import ( "time" configv1 "github.com/openshift/api/config/v1" + features "github.com/openshift/api/features" configclient "github.com/openshift/client-go/config/clientset/versioned" configinformers "github.com/openshift/client-go/config/informers/externalversions" "github.com/openshift/library-go/pkg/controller/controllercmd" @@ -15,6 +16,7 @@ import ( "github.com/openshift/library-go/pkg/operator/loglevel" "github.com/openshift/library-go/pkg/operator/management" "github.com/openshift/library-go/pkg/operator/managementstatecontroller" + pkipkg "github.com/openshift/library-go/pkg/pki" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -27,6 +29,7 @@ import ( cnoclient "github.com/openshift/cluster-network-operator/pkg/client" "github.com/openshift/cluster-network-operator/pkg/controller" "github.com/openshift/cluster-network-operator/pkg/controller/connectivitycheck" + pkictrl "github.com/openshift/cluster-network-operator/pkg/controller/pki" "github.com/openshift/cluster-network-operator/pkg/controller/statusmanager" "github.com/openshift/cluster-network-operator/pkg/hypershift" "github.com/openshift/cluster-network-operator/pkg/names" @@ -140,12 +143,35 @@ func RunOperator(ctx context.Context, controllerConfig *controllercmd.Controller return err } + // Set up PKI profile provider if ConfigurablePKI is enabled. + // Register the PKI informer on the existing configInformers factory, + // re-start to pick it up (idempotent for already-running informers), + // and wait for cache sync before controllers start reconciling. + var pkiProfileProvider pkipkg.PKIProfileProvider + if featureGates.Enabled(features.FeatureGateConfigurablePKI) { + configInformers.Config().V1alpha1().PKIs().Informer() + configInformers.Start(wait.NeverStop) + for t, synced := range configInformers.WaitForCacheSync(wait.NeverStop) { + if !synced { + return fmt.Errorf("failed to sync config informer for %v", t) + } + } + pkiProfileProvider = pkipkg.NewClusterPKIProfileProvider( + configInformers.Config().V1alpha1().PKIs().Lister(), + ) + } + // Add controller-runtime controllers klog.Info("Adding controller-runtime controllers") if err := controller.AddToManager(o.manager, o.StatusManager, o.client, featureGates); err != nil { return fmt.Errorf("failed to add controllers to manager: %w", err) } + // Add PKI controller separately — it needs the PKI profile provider + if err := pkictrl.Add(o.manager, o.StatusManager, featureGates, pkiProfileProvider); err != nil { + return fmt.Errorf("failed to add pki controller: %w", err) + } + // Initialize individual (non-controller-runtime) controllers // logLevelController reacts to changes in the operator spec loglevel diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/cert_config.go b/vendor/github.com/openshift/library-go/pkg/crypto/cert_config.go new file mode 100644 index 0000000000..76d71da2ab --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/crypto/cert_config.go @@ -0,0 +1,225 @@ +package crypto + +import ( + "crypto" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/authentication/user" +) + +// KeyPairGenerator generates a cryptographic key pair. +type KeyPairGenerator interface { + GenerateKeyPair() (crypto.PublicKey, crypto.PrivateKey, error) +} + +// NewSigningCertificate creates a CA certificate. +// By default it creates a self-signed root CA. Use WithSigner to create an +// intermediate CA signed by a parent CA. +// The name parameter is used as the CommonName unless overridden with WithSubject. +// Optional: WithSigner, WithSubject, WithLifetime (defaults to DefaultCACertificateLifetimeDuration). +func NewSigningCertificate(name string, keyGen KeyPairGenerator, opts ...CertificateOption) (*TLSCertificateConfig, error) { + o := &CertificateOptions{ + lifetime: DefaultCACertificateLifetimeDuration, + } + for _, opt := range opts { + opt(o) + } + + subject := pkix.Name{CommonName: name} + if o.subject != nil { + subject = *o.subject + } + + publicKey, privateKey, err := keyGen.GenerateKeyPair() + if err != nil { + return nil, fmt.Errorf("failed to generate key pair: %w", err) + } + subjectKeyId, err := SubjectKeyIDFromPublicKey(publicKey) + if err != nil { + return nil, fmt.Errorf("failed to compute subject key ID: %w", err) + } + + if o.signer != nil { + // Intermediate CA signed by the provided signer. + authorityKeyId := o.signer.Config.Certs[0].SubjectKeyId + template := newSigningCertificateTemplateForDuration(subject, o.lifetime, time.Now, authorityKeyId, subjectKeyId) + template.SignatureAlgorithm = 0 + template.KeyUsage = KeyUsageForPublicKey(publicKey) | x509.KeyUsageCertSign + + cert, err := o.signer.SignCertificate(template, publicKey) + if err != nil { + return nil, fmt.Errorf("failed to sign certificate: %w", err) + } + + return &TLSCertificateConfig{ + Certs: append([]*x509.Certificate{cert}, o.signer.Config.Certs...), + Key: privateKey, + }, nil + } + + // Self-signed root CA. AuthorityKeyId and SubjectKeyId match. + template := newSigningCertificateTemplateForDuration(subject, o.lifetime, time.Now, subjectKeyId, subjectKeyId) + template.SignatureAlgorithm = 0 + template.KeyUsage = KeyUsageForPublicKey(publicKey) | x509.KeyUsageCertSign + + cert, err := signCertificate(template, publicKey, template, privateKey) + if err != nil { + return nil, fmt.Errorf("failed to sign certificate: %w", err) + } + + return &TLSCertificateConfig{ + Certs: []*x509.Certificate{cert}, + Key: privateKey, + }, nil +} + +// NewServerCertificate creates a server/serving certificate signed by this CA. +// Optional: WithLifetime (defaults to DefaultCertificateLifetimeDuration), WithExtensions. +func (ca *CA) NewServerCertificate(hostnames sets.Set[string], keyGen KeyPairGenerator, opts ...CertificateOption) (*TLSCertificateConfig, error) { + o := &CertificateOptions{ + lifetime: DefaultCertificateLifetimeDuration, + } + for _, opt := range opts { + opt(o) + } + + publicKey, privateKey, err := keyGen.GenerateKeyPair() + if err != nil { + return nil, fmt.Errorf("failed to generate key pair: %w", err) + } + subjectKeyId, err := SubjectKeyIDFromPublicKey(publicKey) + if err != nil { + return nil, fmt.Errorf("failed to compute subject key ID: %w", err) + } + + sortedHostnames := sets.List(hostnames) + authorityKeyId := ca.Config.Certs[0].SubjectKeyId + template := newServerCertificateTemplateForDuration( + pkix.Name{CommonName: sortedHostnames[0]}, + sortedHostnames, + o.lifetime, + time.Now, + authorityKeyId, + subjectKeyId, + ) + // Let x509.CreateCertificate auto-detect the signature algorithm from the CA's key. + template.SignatureAlgorithm = 0 + template.KeyUsage = KeyUsageForPublicKey(publicKey) + + for _, fn := range o.extensionFns { + if err := fn(template); err != nil { + return nil, fmt.Errorf("failed to apply certificate extension: %w", err) + } + } + + cert, err := ca.SignCertificate(template, publicKey) + if err != nil { + return nil, fmt.Errorf("failed to sign certificate: %w", err) + } + + return &TLSCertificateConfig{ + Certs: append([]*x509.Certificate{cert}, ca.Config.Certs...), + Key: privateKey, + }, nil +} + +// NewClientCertificate creates a client certificate signed by this CA. +// Optional: WithLifetime (defaults to DefaultCertificateLifetimeDuration). +func (ca *CA) NewClientCertificate(u user.Info, keyGen KeyPairGenerator, opts ...CertificateOption) (*TLSCertificateConfig, error) { + o := &CertificateOptions{ + lifetime: DefaultCertificateLifetimeDuration, + } + for _, opt := range opts { + opt(o) + } + + publicKey, privateKey, err := keyGen.GenerateKeyPair() + if err != nil { + return nil, fmt.Errorf("failed to generate key pair: %w", err) + } + subjectKeyId, err := SubjectKeyIDFromPublicKey(publicKey) + if err != nil { + return nil, fmt.Errorf("failed to compute subject key ID: %w", err) + } + + authorityKeyId := ca.Config.Certs[0].SubjectKeyId + template := NewClientCertificateTemplateForDuration(UserToSubject(u), o.lifetime, time.Now) + template.AuthorityKeyId = authorityKeyId + template.SubjectKeyId = subjectKeyId + // Let x509.CreateCertificate auto-detect the signature algorithm from the CA's key. + template.SignatureAlgorithm = 0 + template.KeyUsage = KeyUsageForPublicKey(publicKey) + + cert, err := ca.SignCertificate(template, publicKey) + if err != nil { + return nil, fmt.Errorf("failed to sign certificate: %w", err) + } + + return &TLSCertificateConfig{ + Certs: append([]*x509.Certificate{cert}, ca.Config.Certs...), + Key: privateKey, + }, nil +} + +// NewPeerCertificate creates a peer certificate (both server and client auth) +// signed by this CA. +// Optional: WithLifetime (defaults to DefaultCertificateLifetimeDuration), WithExtensions. +func (ca *CA) NewPeerCertificate(hostnames sets.Set[string], u user.Info, keyGen KeyPairGenerator, opts ...CertificateOption) (*TLSCertificateConfig, error) { + o := &CertificateOptions{ + lifetime: DefaultCertificateLifetimeDuration, + } + for _, opt := range opts { + opt(o) + } + + publicKey, privateKey, err := keyGen.GenerateKeyPair() + if err != nil { + return nil, fmt.Errorf("failed to generate key pair: %w", err) + } + subjectKeyId, err := SubjectKeyIDFromPublicKey(publicKey) + if err != nil { + return nil, fmt.Errorf("failed to compute subject key ID: %w", err) + } + + sortedHostnames := sets.List(hostnames) + authorityKeyId := ca.Config.Certs[0].SubjectKeyId + + // Start from a server certificate template for the hostnames. + template := newServerCertificateTemplateForDuration( + pkix.Name{CommonName: sortedHostnames[0]}, + sortedHostnames, + o.lifetime, + time.Now, + authorityKeyId, + subjectKeyId, + ) + // Let x509.CreateCertificate auto-detect the signature algorithm from the CA's key. + template.SignatureAlgorithm = 0 + template.KeyUsage = KeyUsageForPublicKey(publicKey) + + // Set subject from user info for client authentication. + template.Subject = UserToSubject(u) + + // Enable both server and client authentication. + template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} + + for _, fn := range o.extensionFns { + if err := fn(template); err != nil { + return nil, fmt.Errorf("failed to apply certificate extension: %w", err) + } + } + + cert, err := ca.SignCertificate(template, publicKey) + if err != nil { + return nil, fmt.Errorf("failed to sign certificate: %w", err) + } + + return &TLSCertificateConfig{ + Certs: append([]*x509.Certificate{cert}, ca.Config.Certs...), + Key: privateKey, + }, nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/keygen.go b/vendor/github.com/openshift/library-go/pkg/crypto/keygen.go new file mode 100644 index 0000000000..96b7559468 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/crypto/keygen.go @@ -0,0 +1,118 @@ +package crypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "fmt" +) + +// KeyAlgorithm identifies the key generation algorithm. +type KeyAlgorithm string + +const ( + // RSAKeyAlgorithm specifies RSA key generation. + RSAKeyAlgorithm KeyAlgorithm = "RSA" + // ECDSAKeyAlgorithm specifies ECDSA key generation. + ECDSAKeyAlgorithm KeyAlgorithm = "ECDSA" +) + +// ECDSACurve identifies a named ECDSA curve. +type ECDSACurve string + +const ( + // P256 specifies the NIST P-256 curve (secp256r1), providing 128-bit security. + P256 ECDSACurve = "P256" + // P384 specifies the NIST P-384 curve (secp384r1), providing 192-bit security. + P384 ECDSACurve = "P384" + // P521 specifies the NIST P-521 curve (secp521r1), providing 256-bit security. + P521 ECDSACurve = "P521" +) + +// RSAKeyPairGenerator generates RSA key pairs. +type RSAKeyPairGenerator struct { + // Bits is the RSA key size in bits. Must be >= 2048. + Bits int +} + +func (g RSAKeyPairGenerator) GenerateKeyPair() (crypto.PublicKey, crypto.PrivateKey, error) { + bits := g.Bits + if bits == 0 { + bits = keyBits + } + privateKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return &privateKey.PublicKey, privateKey, nil +} + +// ECDSAKeyPairGenerator generates ECDSA key pairs. +type ECDSAKeyPairGenerator struct { + // Curve is the named ECDSA curve. + Curve ECDSACurve +} + +func (g ECDSAKeyPairGenerator) GenerateKeyPair() (crypto.PublicKey, crypto.PrivateKey, error) { + curve, err := g.ellipticCurve() + if err != nil { + return nil, nil, err + } + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, nil, err + } + return &privateKey.PublicKey, privateKey, nil +} + +func (g ECDSAKeyPairGenerator) ellipticCurve() (elliptic.Curve, error) { + switch g.Curve { + case P256: + return elliptic.P256(), nil + case P384: + return elliptic.P384(), nil + case P521: + return elliptic.P521(), nil + default: + return nil, fmt.Errorf("unsupported ECDSA curve: %q", g.Curve) + } +} + +// KeyUsageForPublicKey returns the x509.KeyUsage flags appropriate for the +// given public key type. ECDSA keys use DigitalSignature only; RSA keys also +// include KeyEncipherment. +func KeyUsageForPublicKey(pub crypto.PublicKey) x509.KeyUsage { + switch pub.(type) { + case *ecdsa.PublicKey: + return x509.KeyUsageDigitalSignature + default: + return x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature + } +} + +// SubjectKeyIDFromPublicKey computes a truncated SHA-256 hash suitable for +// use as a certificate SubjectKeyId from any supported public key type. +// This uses the first 160 bits of the SHA-256 hash per RFC 7093, consistent +// with the Go standard library since Go 1.25 (go.dev/issue/71746) and +// Let's Encrypt. Prior Go versions used SHA-1 which is not FIPS-compatible. +func SubjectKeyIDFromPublicKey(pub crypto.PublicKey) ([]byte, error) { + var rawBytes []byte + switch pub := pub.(type) { + case *rsa.PublicKey: + rawBytes = pub.N.Bytes() + case *ecdsa.PublicKey: + ecdhKey, err := pub.ECDH() + if err != nil { + return nil, fmt.Errorf("failed to convert ECDSA public key: %w", err) + } + rawBytes = ecdhKey.Bytes() + default: + return nil, fmt.Errorf("unsupported public key type: %T", pub) + } + hash := sha256.Sum256(rawBytes) + return hash[:20], nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/options.go b/vendor/github.com/openshift/library-go/pkg/crypto/options.go new file mode 100644 index 0000000000..983b115d22 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/crypto/options.go @@ -0,0 +1,49 @@ +package crypto + +import ( + "crypto/x509/pkix" + "time" +) + +// CertificateOptions holds optional configuration collected from functional options. +type CertificateOptions struct { + lifetime time.Duration + subject *pkix.Name + extensionFns []CertificateExtensionFunc + signer *CA +} + +// CertificateOption is a functional option for certificate creation. +type CertificateOption func(*CertificateOptions) + +// WithLifetime sets the certificate lifetime duration. +func WithLifetime(d time.Duration) CertificateOption { + return func(o *CertificateOptions) { + o.lifetime = d + } +} + +// WithSubject overrides the certificate subject. For signing certificates, +// this overrides the default subject derived from the name parameter. +func WithSubject(s pkix.Name) CertificateOption { + return func(o *CertificateOptions) { + o.subject = &s + } +} + +// WithSigner specifies a CA to sign the certificate. When used with +// NewSigningCertificate, this creates an intermediate CA signed by the +// given CA instead of a self-signed root CA. +func WithSigner(ca *CA) CertificateOption { + return func(o *CertificateOptions) { + o.signer = ca + } +} + +// WithExtensions adds certificate extension functions that are called +// to modify the certificate template before signing. +func WithExtensions(fns ...CertificateExtensionFunc) CertificateOption { + return func(o *CertificateOptions) { + o.extensionFns = append(o.extensionFns, fns...) + } +} diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/tls_adherence.go b/vendor/github.com/openshift/library-go/pkg/crypto/tls_adherence.go new file mode 100644 index 0000000000..ef0e1af51a --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/crypto/tls_adherence.go @@ -0,0 +1,23 @@ +package crypto + +import ( + configv1 "github.com/openshift/api/config/v1" +) + +// ShouldHonorClusterTLSProfile returns true if the component should honor the +// cluster-wide TLS security profile settings from apiserver.config.openshift.io/cluster. +// +// When this returns true (StrictAllComponents mode), components must honor the +// cluster-wide TLS profile unless they have a component-specific TLS configuration +// that overrides it. +// +// Unknown enum values are treated as StrictAllComponents for forward compatibility +// and to default to the more secure behavior. +func ShouldHonorClusterTLSProfile(tlsAdherence configv1.TLSAdherencePolicy) bool { + switch tlsAdherence { + case configv1.TLSAdherencePolicyNoOpinion, configv1.TLSAdherencePolicyLegacyAdheringComponentsOnly: + return false + default: + return true + } +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/client_cert_rotation_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/client_cert_rotation_controller.go index f9ad1fc14b..dbacb066e9 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/client_cert_rotation_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/client_cert_rotation_controller.go @@ -6,13 +6,12 @@ import ( "time" operatorv1 "github.com/openshift/api/operator/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/wait" - "github.com/openshift/library-go/pkg/controller/factory" "github.com/openshift/library-go/pkg/operator/condition" "github.com/openshift/library-go/pkg/operator/events" "github.com/openshift/library-go/pkg/operator/v1helpers" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/wait" ) const ( @@ -80,6 +79,7 @@ func NewCertRotationController( RotatedSelfSignedCertKeySecret: rotatedSelfSignedCertKeySecret, StatusReporter: reporter, } + return factory.New(). ResyncEvery(time.Minute). WithSync(c.Sync). diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go index c2c8b8368f..019cce7d5a 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go @@ -9,6 +9,7 @@ import ( "github.com/openshift/library-go/pkg/crypto" "github.com/openshift/library-go/pkg/operator/events" "github.com/openshift/library-go/pkg/operator/resource/resourcehelper" + "github.com/openshift/library-go/pkg/pki" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -48,6 +49,13 @@ type RotatedSigningCASecret struct { // AdditionalAnnotations is a collection of annotations set for the secret AdditionalAnnotations AdditionalAnnotations + // CertificateName is the logical name of this certificate for PKI profile resolution. + CertificateName string + + // PKIProfileProvider, when non-nil, enables ConfigurablePKI certificate + // key algorithm resolution. When nil, legacy certificate generation is used. + PKIProfileProvider pki.PKIProfileProvider + // Plumbing: Informer corev1informers.SecretInformer Lister corev1listers.SecretLister @@ -87,12 +95,12 @@ func (c RotatedSigningCASecret) EnsureSigningCertKeyPair(ctx context.Context) (* // run Update if signer content needs changing signerUpdated := false - if needed, reason := needNewSigningCertKeyPair(signingCertKeyPairSecret, c.Refresh, c.RefreshOnlyWhenExpired); needed || creationRequired { + if needed, reason := c.needNewSigningCertKeyPair(signingCertKeyPairSecret); needed || creationRequired { if creationRequired { reason = "secret doesn't exist" } c.EventRecorder.Eventf("SignerUpdateRequired", "%q in %q requires a new signing cert/key pair: %v", c.Name, c.Namespace, reason) - if err = setSigningCertKeyPairSecretAndTLSAnnotations(signingCertKeyPairSecret, c.Validity, c.Refresh, c.AdditionalAnnotations); err != nil { + if err = c.setSigningCertKeyPairSecretAndTLSAnnotations(signingCertKeyPairSecret); err != nil { return nil, false, err } @@ -149,7 +157,7 @@ func ensureOwnerReference(meta *metav1.ObjectMeta, owner *metav1.OwnerReference) return false } -func needNewSigningCertKeyPair(secret *corev1.Secret, refresh time.Duration, refreshOnlyWhenExpired bool) (bool, string) { +func (c RotatedSigningCASecret) needNewSigningCertKeyPair(secret *corev1.Secret) (bool, string) { annotations := secret.Annotations notBefore, notAfter, reason := getValidityFromAnnotations(annotations) if len(reason) > 0 { @@ -160,7 +168,7 @@ func needNewSigningCertKeyPair(secret *corev1.Secret, refresh time.Duration, ref return true, "already expired" } - if refreshOnlyWhenExpired { + if c.RefreshOnlyWhenExpired { return false, "" } @@ -170,7 +178,7 @@ func needNewSigningCertKeyPair(secret *corev1.Secret, refresh time.Duration, ref return true, fmt.Sprintf("past refresh time (80%% of validity): %v", at80Percent) } - developerSpecifiedRefresh := notBefore.Add(refresh) + developerSpecifiedRefresh := notBefore.Add(c.Refresh) if time.Now().After(developerSpecifiedRefresh) { return true, fmt.Sprintf("past its refresh time %v", developerSpecifiedRefresh) } @@ -199,22 +207,67 @@ func getValidityFromAnnotations(annotations map[string]string) (notBefore time.T return notBefore, notAfter, "" } +func (c RotatedSigningCASecret) resolveKeyPairGenerator() (crypto.KeyPairGenerator, error) { + return resolveKeyPairGeneratorWithFallback(c.PKIProfileProvider, pki.CertificateTypeSigner, c.CertificateName) +} + +// resolveKeyPairGeneratorWithFallback resolves the key pair generator from the +// PKI profile provider. Returns nil for Unmanaged mode (no key override). +// +// TODO(sanchezl): Remove the fallback to DefaultPKIProfile() once installer +// support for the PKI resource is in place. Until then, the PKI resource may +// not exist in TechPreview clusters. Once removed, callers can use +// pki.ResolveCertificateConfig directly. +func resolveKeyPairGeneratorWithFallback(provider pki.PKIProfileProvider, certType pki.CertificateType, name string) (crypto.KeyPairGenerator, error) { + cfg, err := pki.ResolveCertificateConfig(provider, certType, name) + if err != nil { + klog.Warningf("Failed to resolve PKI config for %s %q, falling back to default profile: %v", certType, name, err) + defaultProfile := pki.DefaultPKIProfile() + cfg, err = pki.ResolveCertificateConfig(pki.NewStaticPKIProfileProvider(&defaultProfile), certType, name) + if err != nil { + return nil, err + } + } + if cfg == nil { + return nil, nil + } + return cfg.Key, nil +} + // setSigningCertKeyPairSecretAndTLSAnnotations generates a new signing certificate and key pair, // stores them in the specified secret, and adds predefined TLS annotations to that secret. -func setSigningCertKeyPairSecretAndTLSAnnotations(signingCertKeyPairSecret *corev1.Secret, validity, refresh time.Duration, tlsAnnotations AdditionalAnnotations) error { - ca, err := setSigningCertKeyPairSecret(signingCertKeyPairSecret, validity) +func (c RotatedSigningCASecret) setSigningCertKeyPairSecretAndTLSAnnotations(signingCertKeyPairSecret *corev1.Secret) error { + ca, err := c.setSigningCertKeyPairSecret(signingCertKeyPairSecret) if err != nil { return err } - setTLSAnnotationsOnSigningCertKeyPairSecret(signingCertKeyPairSecret, ca, refresh, tlsAnnotations) + c.setTLSAnnotationsOnSigningCertKeyPairSecret(signingCertKeyPairSecret, ca) return nil } -// setSigningCertKeyPairSecret creates a new signing cert/key pair and sets them in the secret -func setSigningCertKeyPairSecret(signingCertKeyPairSecret *corev1.Secret, validity time.Duration) (*crypto.TLSCertificateConfig, error) { +// setSigningCertKeyPairSecret creates a new signing cert/key pair and sets them in the secret. +func (c RotatedSigningCASecret) setSigningCertKeyPairSecret(signingCertKeyPairSecret *corev1.Secret) (*crypto.TLSCertificateConfig, error) { signerName := fmt.Sprintf("%s_%s@%d", signingCertKeyPairSecret.Namespace, signingCertKeyPairSecret.Name, time.Now().Unix()) - ca, err := crypto.MakeSelfSignedCAConfigForDuration(signerName, validity) + + var ca *crypto.TLSCertificateConfig + var err error + if c.PKIProfileProvider != nil { + keyGen, err := c.resolveKeyPairGenerator() + if err != nil { + return nil, err + } + if keyGen != nil { + ca, err = crypto.NewSigningCertificate(signerName, keyGen, crypto.WithLifetime(c.Validity)) + if err != nil { + return nil, err + } + } + // nil keyGen means Unmanaged: fall through to legacy cert generation + } + if ca == nil { + ca, err = crypto.MakeSelfSignedCAConfigForDuration(signerName, c.Validity) + } if err != nil { return nil, err } @@ -243,11 +296,12 @@ func setSigningCertKeyPairSecret(signingCertKeyPairSecret *corev1.Secret, validi // // These assumptions are safe because this function is only called after the secret // has been initialized in setSigningCertKeyPairSecret. -func setTLSAnnotationsOnSigningCertKeyPairSecret(signingCertKeyPairSecret *corev1.Secret, ca *crypto.TLSCertificateConfig, refresh time.Duration, tlsAnnotations AdditionalAnnotations) { +func (c RotatedSigningCASecret) setTLSAnnotationsOnSigningCertKeyPairSecret(signingCertKeyPairSecret *corev1.Secret, ca *crypto.TLSCertificateConfig) { signingCertKeyPairSecret.Annotations[CertificateIssuer] = ca.Certs[0].Issuer.CommonName + tlsAnnotations := c.AdditionalAnnotations tlsAnnotations.NotBefore = ca.Certs[0].NotBefore.Format(time.RFC3339) tlsAnnotations.NotAfter = ca.Certs[0].NotAfter.Format(time.RFC3339) - tlsAnnotations.RefreshPeriod = refresh.String() + tlsAnnotations.RefreshPeriod = c.Refresh.String() _ = tlsAnnotations.EnsureTLSMetadataUpdate(&signingCertKeyPairSecret.ObjectMeta) } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/target.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/target.go index 88cd41189e..b76c494402 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/target.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/target.go @@ -18,6 +18,7 @@ import ( "github.com/openshift/library-go/pkg/crypto" "github.com/openshift/library-go/pkg/operator/events" "github.com/openshift/library-go/pkg/operator/resource/resourcehelper" + "github.com/openshift/library-go/pkg/pki" corev1informers "k8s.io/client-go/informers/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1listers "k8s.io/client-go/listers/core/v1" @@ -64,6 +65,13 @@ type RotatedSelfSignedCertKeySecret struct { // CertCreator does the actual cert generation. CertCreator TargetCertCreator + // CertificateName is the logical name of this certificate for PKI profile resolution. + CertificateName string + + // PKIProfileProvider, when non-nil, enables ConfigurablePKI certificate + // key algorithm resolution. When nil, legacy certificate generation is used. + PKIProfileProvider pki.PKIProfileProvider + // Plumbing: Informer corev1informers.SecretInformer Lister corev1listers.SecretLister @@ -72,12 +80,15 @@ type RotatedSelfSignedCertKeySecret struct { } type TargetCertCreator interface { - // NewCertificate creates a new key-cert pair with the given signer. - NewCertificate(signer *crypto.CA, validity time.Duration) (*crypto.TLSCertificateConfig, error) + // NewCertificate creates a new key-cert pair with the given signer. If keyGen + // is non-nil, it is used to generate the key pair; otherwise legacy defaults are used. + NewCertificate(signer *crypto.CA, validity time.Duration, keyGen crypto.KeyPairGenerator) (*crypto.TLSCertificateConfig, error) // NeedNewTargetCertKeyPair decides whether a new cert-key pair is needed. It returns a non-empty reason if it is the case. NeedNewTargetCertKeyPair(currentCertSecret *corev1.Secret, signer *crypto.CA, caBundleCerts []*x509.Certificate, refresh time.Duration, refreshOnlyWhenExpired, creationRequired bool) string // SetAnnotations gives an option to override or set additional annotations SetAnnotations(cert *crypto.TLSCertificateConfig, annotations map[string]string) map[string]string + // CertificateType returns the category of certificate this creator produces. + CertificateType() pki.CertificateType } // TargetCertRechecker is an optional interface to be implemented by the TargetCertCreator to enforce @@ -121,7 +132,7 @@ func (c RotatedSelfSignedCertKeySecret) EnsureTargetCertKeyPair(ctx context.Cont if reason := c.CertCreator.NeedNewTargetCertKeyPair(targetCertKeyPairSecret, signingCertKeyPair, caBundleCerts, c.Refresh, c.RefreshOnlyWhenExpired, creationRequired); len(reason) > 0 { c.EventRecorder.Eventf("TargetUpdateRequired", "%q in %q requires a new target cert/key pair: %v", c.Name, c.Namespace, reason) - if err = setTargetCertKeyPairSecretAndTLSAnnotations(targetCertKeyPairSecret, c.Validity, c.Refresh, signingCertKeyPair, c.CertCreator, c.AdditionalAnnotations); err != nil { + if err = c.setTargetCertKeyPairSecretAndTLSAnnotations(targetCertKeyPairSecret, signingCertKeyPair); err != nil { return nil, err } @@ -239,19 +250,19 @@ func needNewTargetCertKeyPairForTime(annotations map[string]string, signer *cryp // setTargetCertKeyPairSecretAndTLSAnnotations generates a new cert/key pair, // stores them in the specified secret, and adds predefined TLS annotations to that secret. -func setTargetCertKeyPairSecretAndTLSAnnotations(targetCertKeyPairSecret *corev1.Secret, validity, refresh time.Duration, signer *crypto.CA, certCreator TargetCertCreator, tlsAnnotations AdditionalAnnotations) error { - certKeyPair, err := setTargetCertKeyPairSecret(targetCertKeyPairSecret, validity, signer, certCreator) +func (c RotatedSelfSignedCertKeySecret) setTargetCertKeyPairSecretAndTLSAnnotations(targetCertKeyPairSecret *corev1.Secret, signer *crypto.CA) error { + certKeyPair, err := c.setTargetCertKeyPairSecret(targetCertKeyPairSecret, signer) if err != nil { return err } - setTLSAnnotationsOnTargetCertKeyPairSecret(targetCertKeyPairSecret, certKeyPair, certCreator, refresh, tlsAnnotations) + c.setTLSAnnotationsOnTargetCertKeyPairSecret(targetCertKeyPairSecret, certKeyPair) return nil } // setTargetCertKeyPairSecret creates a new cert/key pair and sets them in the secret. Only one of client, serving, or signer rotation may be specified. // TODO refactor with an interface for actually signing and move the one-of check higher in the stack. -func setTargetCertKeyPairSecret(targetCertKeyPairSecret *corev1.Secret, validity time.Duration, signer *crypto.CA, certCreator TargetCertCreator) (*crypto.TLSCertificateConfig, error) { +func (c RotatedSelfSignedCertKeySecret) setTargetCertKeyPairSecret(targetCertKeyPairSecret *corev1.Secret, signer *crypto.CA) (*crypto.TLSCertificateConfig, error) { if targetCertKeyPairSecret.Annotations == nil { targetCertKeyPairSecret.Annotations = map[string]string{} } @@ -260,13 +271,22 @@ func setTargetCertKeyPairSecret(targetCertKeyPairSecret *corev1.Secret, validity } // our annotation is based on our cert validity, so we want to make sure that we don't specify something past our signer - targetValidity := validity + targetValidity := c.Validity remainingSignerValidity := signer.Config.Certs[0].NotAfter.Sub(time.Now()) - if remainingSignerValidity < validity { + if remainingSignerValidity < targetValidity { targetValidity = remainingSignerValidity } - certKeyPair, err := certCreator.NewCertificate(signer, targetValidity) + var keyGen crypto.KeyPairGenerator + if c.PKIProfileProvider != nil { + var err error + keyGen, err = c.resolveKeyPairGenerator() + if err != nil { + return nil, err + } + } + + certKeyPair, err := c.CertCreator.NewCertificate(signer, targetValidity, keyGen) if err != nil { return nil, err } @@ -282,22 +302,34 @@ func setTargetCertKeyPairSecret(targetCertKeyPairSecret *corev1.Secret, validity // // These assumptions are safe because this function is only called after the secret // has been initialized in setTargetCertKeyPairSecret. -func setTLSAnnotationsOnTargetCertKeyPairSecret(targetCertKeyPairSecret *corev1.Secret, certKeyPair *crypto.TLSCertificateConfig, certCreator TargetCertCreator, refresh time.Duration, tlsAnnotations AdditionalAnnotations) { +func (c RotatedSelfSignedCertKeySecret) setTLSAnnotationsOnTargetCertKeyPairSecret(targetCertKeyPairSecret *corev1.Secret, certKeyPair *crypto.TLSCertificateConfig) { targetCertKeyPairSecret.Annotations[CertificateIssuer] = certKeyPair.Certs[0].Issuer.CommonName + tlsAnnotations := c.AdditionalAnnotations tlsAnnotations.NotBefore = certKeyPair.Certs[0].NotBefore.Format(time.RFC3339) tlsAnnotations.NotAfter = certKeyPair.Certs[0].NotAfter.Format(time.RFC3339) - tlsAnnotations.RefreshPeriod = refresh.String() + tlsAnnotations.RefreshPeriod = c.Refresh.String() _ = tlsAnnotations.EnsureTLSMetadataUpdate(&targetCertKeyPairSecret.ObjectMeta) - certCreator.SetAnnotations(certKeyPair, targetCertKeyPairSecret.Annotations) + c.CertCreator.SetAnnotations(certKeyPair, targetCertKeyPairSecret.Annotations) +} + +func (c RotatedSelfSignedCertKeySecret) resolveKeyPairGenerator() (crypto.KeyPairGenerator, error) { + return resolveKeyPairGeneratorWithFallback(c.PKIProfileProvider, c.CertCreator.CertificateType(), c.CertificateName) } type ClientRotation struct { UserInfo user.Info } -func (r *ClientRotation) NewCertificate(signer *crypto.CA, validity time.Duration) (*crypto.TLSCertificateConfig, error) { +func (r *ClientRotation) CertificateType() pki.CertificateType { + return pki.CertificateTypeClient +} + +func (r *ClientRotation) NewCertificate(signer *crypto.CA, validity time.Duration, keyGen crypto.KeyPairGenerator) (*crypto.TLSCertificateConfig, error) { + if keyGen != nil { + return signer.NewClientCertificate(r.UserInfo, keyGen, crypto.WithLifetime(validity)) + } return signer.MakeClientCertificateForDuration(r.UserInfo, validity) } @@ -315,11 +347,23 @@ type ServingRotation struct { HostnamesChanged <-chan struct{} } -func (r *ServingRotation) NewCertificate(signer *crypto.CA, validity time.Duration) (*crypto.TLSCertificateConfig, error) { - if len(r.Hostnames()) == 0 { +func (r *ServingRotation) CertificateType() pki.CertificateType { + return pki.CertificateTypeServing +} + +func (r *ServingRotation) NewCertificate(signer *crypto.CA, validity time.Duration, keyGen crypto.KeyPairGenerator) (*crypto.TLSCertificateConfig, error) { + hostnames := r.Hostnames() + if len(hostnames) == 0 { return nil, fmt.Errorf("no hostnames set") } - return signer.MakeServerCertForDuration(sets.New(r.Hostnames()...), validity, r.CertificateExtensionFn...) + if keyGen != nil { + return signer.NewServerCertificate( + sets.New(hostnames...), keyGen, + crypto.WithLifetime(validity), + crypto.WithExtensions(r.CertificateExtensionFn...), + ) + } + return signer.MakeServerCertForDuration(sets.New(hostnames...), validity, r.CertificateExtensionFn...) } func (r *ServingRotation) RecheckChannel() <-chan struct{} { @@ -336,18 +380,25 @@ func (r *ServingRotation) NeedNewTargetCertKeyPair(currentCertSecret *corev1.Sec } func (r *ServingRotation) missingHostnames(annotations map[string]string) string { + return missingHostnames(annotations, r.Hostnames()) +} + +func (r *ServingRotation) SetAnnotations(cert *crypto.TLSCertificateConfig, annotations map[string]string) map[string]string { + return setHostnameAnnotations(cert, annotations) +} + +func missingHostnames(annotations map[string]string, hostnames []string) string { existingHostnames := sets.New(strings.Split(annotations[CertificateHostnames], ",")...) - requiredHostnames := sets.New(r.Hostnames()...) + requiredHostnames := sets.New(hostnames...) if !existingHostnames.Equal(requiredHostnames) { existingNotRequired := existingHostnames.Difference(requiredHostnames) requiredNotExisting := requiredHostnames.Difference(existingHostnames) return fmt.Sprintf("%q are existing and not required, %q are required and not existing", strings.Join(sets.List(existingNotRequired), ","), strings.Join(sets.List(requiredNotExisting), ",")) } - return "" } -func (r *ServingRotation) SetAnnotations(cert *crypto.TLSCertificateConfig, annotations map[string]string) map[string]string { +func setHostnameAnnotations(cert *crypto.TLSCertificateConfig, annotations map[string]string) map[string]string { hostnames := sets.Set[string]{} for _, ip := range cert.Certs[0].IPAddresses { hostnames.Insert(ip.String()) @@ -355,7 +406,6 @@ func (r *ServingRotation) SetAnnotations(cert *crypto.TLSCertificateConfig, anno for _, dnsName := range cert.Certs[0].DNSNames { hostnames.Insert(dnsName) } - // List does a sort so that we have a consistent representation annotations[CertificateHostnames] = strings.Join(sets.List(hostnames), ",") return annotations @@ -367,11 +417,81 @@ type SignerRotation struct { SignerName string } -func (r *SignerRotation) NewCertificate(signer *crypto.CA, validity time.Duration) (*crypto.TLSCertificateConfig, error) { +func (r *SignerRotation) CertificateType() pki.CertificateType { + return pki.CertificateTypeSigner +} + +func (r *SignerRotation) NewCertificate(signer *crypto.CA, validity time.Duration, keyGen crypto.KeyPairGenerator) (*crypto.TLSCertificateConfig, error) { signerName := fmt.Sprintf("%s_@%d", r.SignerName, time.Now().Unix()) + if keyGen != nil { + return crypto.NewSigningCertificate(signerName, keyGen, + crypto.WithSigner(signer), + crypto.WithLifetime(validity), + ) + } return crypto.MakeCAConfigForDuration(signerName, validity, signer) } +// PeerRotation creates certificates used for both server and client authentication +// (e.g., etcd peer certificates). It uses CertificateTypePeer for PKI profile +// resolution, which selects the stronger of the serving and client key configurations. +// +// When keyGen is non-nil (ConfigurablePKI enabled), it calls NewPeerCertificate +// which requires UserInfo for the client identity. When keyGen is nil (legacy), +// it falls back to MakeServerCertForDuration with an extension function that +// sets both ClientAuth and ServerAuth ExtKeyUsages. +type PeerRotation struct { + Hostnames ServingHostnameFunc + UserInfo user.Info + CertificateExtensionFn []crypto.CertificateExtensionFunc + HostnamesChanged <-chan struct{} +} + +func (r *PeerRotation) CertificateType() pki.CertificateType { + return pki.CertificateTypePeer +} + +func (r *PeerRotation) NewCertificate(signer *crypto.CA, validity time.Duration, keyGen crypto.KeyPairGenerator) (*crypto.TLSCertificateConfig, error) { + hostnames := r.Hostnames() + if len(hostnames) == 0 { + return nil, fmt.Errorf("no hostnames set") + } + if keyGen != nil { + if r.UserInfo == nil { + return nil, fmt.Errorf("PeerRotation requires UserInfo for configurable PKI certificates") + } + return signer.NewPeerCertificate( + sets.New(hostnames...), r.UserInfo, keyGen, + crypto.WithLifetime(validity), + crypto.WithExtensions(r.CertificateExtensionFn...), + ) + } + // Legacy path: use server cert template with extension fn to add both ExtKeyUsages. + // The subject CN comes from the first hostname (preserves current behavior). + peerExtFn := func(cert *x509.Certificate) error { + cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth} + return nil + } + extensions := append(append([]crypto.CertificateExtensionFunc{}, r.CertificateExtensionFn...), peerExtFn) + return signer.MakeServerCertForDuration(sets.New(hostnames...), validity, extensions...) +} + +func (r *PeerRotation) RecheckChannel() <-chan struct{} { + return r.HostnamesChanged +} + +func (r *PeerRotation) NeedNewTargetCertKeyPair(currentCertSecret *corev1.Secret, signer *crypto.CA, caBundleCerts []*x509.Certificate, refresh time.Duration, refreshOnlyWhenExpired, creationRequired bool) string { + reason := needNewTargetCertKeyPair(currentCertSecret, signer, caBundleCerts, refresh, refreshOnlyWhenExpired, creationRequired) + if len(reason) > 0 { + return reason + } + return missingHostnames(currentCertSecret.Annotations, r.Hostnames()) +} + +func (r *PeerRotation) SetAnnotations(cert *crypto.TLSCertificateConfig, annotations map[string]string) map[string]string { + return setHostnameAnnotations(cert, annotations) +} + func (r *SignerRotation) NeedNewTargetCertKeyPair(currentCertSecret *corev1.Secret, signer *crypto.CA, caBundleCerts []*x509.Certificate, refresh time.Duration, refreshOnlyWhenExpired, exists bool) string { return needNewTargetCertKeyPair(currentCertSecret, signer, caBundleCerts, refresh, refreshOnlyWhenExpired, exists) } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/v1helpers/helpers.go b/vendor/github.com/openshift/library-go/pkg/operator/v1helpers/helpers.go index fd34ec6201..f4a0943e51 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/v1helpers/helpers.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/v1helpers/helpers.go @@ -123,11 +123,22 @@ type UpdateOperatorSpecFunc func(spec *operatorv1.OperatorSpec) error func UpdateSpec(ctx context.Context, client OperatorClient, updateFuncs ...UpdateOperatorSpecFunc) (*operatorv1.OperatorSpec, bool, error) { updated := false var operatorSpec *operatorv1.OperatorSpec + previousResourceVersion := "" err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { oldSpec, _, resourceVersion, err := client.GetOperatorState() if err != nil { return err } + if resourceVersion == previousResourceVersion { + // Lister is stale (e.g. after a conflict or restart); do a live GET to get the current resourceVersion. + listerResourceVersion := resourceVersion + oldSpec, _, resourceVersion, err = client.GetOperatorStateWithQuorum(ctx) + if err != nil { + return err + } + klog.V(2).Infof("lister was stale at resourceVersion=%v, live get showed resourceVersion=%v", listerResourceVersion, resourceVersion) + } + previousResourceVersion = resourceVersion newSpec := oldSpec.DeepCopy() for _, update := range updateFuncs { diff --git a/vendor/github.com/openshift/library-go/pkg/pki/profile.go b/vendor/github.com/openshift/library-go/pkg/pki/profile.go new file mode 100644 index 0000000000..6f534c94f0 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/pki/profile.go @@ -0,0 +1,114 @@ +package pki + +import ( + "fmt" + + configv1alpha1 "github.com/openshift/api/config/v1alpha1" + "github.com/openshift/library-go/pkg/crypto" +) + +// DefaultPKIProfile returns the default PKIProfile for OpenShift. +func DefaultPKIProfile() configv1alpha1.PKIProfile { + return configv1alpha1.PKIProfile{ + Defaults: configv1alpha1.DefaultCertificateConfig{ + Key: configv1alpha1.KeyConfig{ + Algorithm: configv1alpha1.KeyAlgorithmECDSA, + ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP256}, + }, + }, + SignerCertificates: configv1alpha1.CertificateConfig{ + Key: configv1alpha1.KeyConfig{ + Algorithm: configv1alpha1.KeyAlgorithmECDSA, + ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP384}, + }, + }, + } +} + +// KeyPairGeneratorFromAPI converts a configv1alpha1.KeyConfig to a +// crypto.KeyPairGenerator. +func KeyPairGeneratorFromAPI(apiKey configv1alpha1.KeyConfig) (crypto.KeyPairGenerator, error) { + switch apiKey.Algorithm { + case configv1alpha1.KeyAlgorithmRSA: + return crypto.RSAKeyPairGenerator{ + Bits: int(apiKey.RSA.KeySize), + }, nil + case configv1alpha1.KeyAlgorithmECDSA: + curve, err := ecdsaCurveFromAPI(apiKey.ECDSA.Curve) + if err != nil { + return nil, err + } + return crypto.ECDSAKeyPairGenerator{ + Curve: curve, + }, nil + default: + return nil, fmt.Errorf("unknown key algorithm: %q", apiKey.Algorithm) + } +} + +// ecdsaCurveFromAPI converts an API ECDSA curve name to the crypto package's ECDSACurve. +func ecdsaCurveFromAPI(c configv1alpha1.ECDSACurve) (crypto.ECDSACurve, error) { + switch c { + case configv1alpha1.ECDSACurveP256: + return crypto.P256, nil + case configv1alpha1.ECDSACurveP384: + return crypto.P384, nil + case configv1alpha1.ECDSACurveP521: + return crypto.P521, nil + default: + return "", fmt.Errorf("unknown ECDSA curve: %q", c) + } +} + +// securityBits returns the NIST security strength in bits for a given +// KeyPairGenerator. For RSA, values come from the rsaSecurityStrength table. +// For ECDSA, security strength is half the key size (fixed per curve). +func securityBits(g crypto.KeyPairGenerator) int { + switch g := g.(type) { + case crypto.RSAKeyPairGenerator: + return rsaSecurityStrength[g.Bits] + case crypto.ECDSAKeyPairGenerator: + switch g.Curve { + case crypto.P256: + return 128 + case crypto.P384: + return 192 + case crypto.P521: + return 256 + } + } + return 0 +} + +// rsaSecurityStrength maps RSA key sizes (2048-8192 in 1024-bit increments) +// to their security strengths from NIST SP 800-56B Rev 2 Table 2 or +// pre-calculated from the GNFS complexity estimate. +var rsaSecurityStrength = map[int]int{ + 2048: 112, + 3072: 128, + 4096: 152, + 5120: 168, + 6144: 176, + 7168: 192, + 8192: 200, +} + +// strongerKeyPairGenerator returns whichever of a or b provides higher NIST +// security strength. In case of a tie, ECDSA is preferred over RSA. +func strongerKeyPairGenerator(a, b crypto.KeyPairGenerator) crypto.KeyPairGenerator { + sa, sb := securityBits(a), securityBits(b) + if sb > sa { + return b + } + if sa > sb { + return a + } + // Equal strength: prefer ECDSA over RSA. + if _, ok := a.(crypto.ECDSAKeyPairGenerator); ok { + return a + } + if _, ok := b.(crypto.ECDSAKeyPairGenerator); ok { + return b + } + return a +} diff --git a/vendor/github.com/openshift/library-go/pkg/pki/provider.go b/vendor/github.com/openshift/library-go/pkg/pki/provider.go new file mode 100644 index 0000000000..2007aa890e --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/pki/provider.go @@ -0,0 +1,75 @@ +package pki + +import ( + "fmt" + + configv1alpha1 "github.com/openshift/api/config/v1alpha1" + configv1alpha1listers "github.com/openshift/client-go/config/listers/config/v1alpha1" +) + +// PKIProfileProvider provides the PKIProfile that determines certificate key +// configuration. A nil profile indicates Unmanaged mode where the caller +// should use its own defaults. +type PKIProfileProvider interface { + PKIProfile() (*configv1alpha1.PKIProfile, error) +} + +// StaticPKIProfileProvider is a PKIProfileProvider backed by a fixed PKIProfile. +type StaticPKIProfileProvider struct { + profile *configv1alpha1.PKIProfile +} + +// NewStaticPKIProfileProvider returns a PKIProfileProvider backed by the given +// profile. A nil profile signals Unmanaged mode. +func NewStaticPKIProfileProvider(profile *configv1alpha1.PKIProfile) *StaticPKIProfileProvider { + return &StaticPKIProfileProvider{profile: profile} +} + +// PKIProfile returns the static PKIProfile. +func (s *StaticPKIProfileProvider) PKIProfile() (*configv1alpha1.PKIProfile, error) { + return s.profile, nil +} + +// ListerPKIProfileProvider is a PKIProfileProvider that reads a named +// cluster-scoped PKI resource via a lister. +type ListerPKIProfileProvider struct { + lister configv1alpha1listers.PKILister + resourceName string +} + +// NewClusterPKIProfileProvider creates a PKIProfileProvider that resolves the +// PKIProfile from the OpenShift cluster configuration PKI resource. +func NewClusterPKIProfileProvider(lister configv1alpha1listers.PKILister) *ListerPKIProfileProvider { + return NewListerPKIProfileProvider(lister, "cluster") +} + +// NewListerPKIProfileProvider returns a PKIProfileProvider that reads the +// named cluster-scoped PKI resource via a lister. +func NewListerPKIProfileProvider(lister configv1alpha1listers.PKILister, resourceName string) *ListerPKIProfileProvider { + return &ListerPKIProfileProvider{ + lister: lister, + resourceName: resourceName, + } +} + +// PKIProfile reads the PKI resource and returns the profile based on its +// certificate management mode. Returns nil for Unmanaged mode. +func (l *ListerPKIProfileProvider) PKIProfile() (*configv1alpha1.PKIProfile, error) { + pki, err := l.lister.Get(l.resourceName) + if err != nil { + return nil, fmt.Errorf("failed to get PKI resource %q: %w", l.resourceName, err) + } + + switch pki.Spec.CertificateManagement.Mode { + case configv1alpha1.PKICertificateManagementModeUnmanaged: + return nil, nil + case configv1alpha1.PKICertificateManagementModeDefault: + profile := DefaultPKIProfile() + return &profile, nil + case configv1alpha1.PKICertificateManagementModeCustom: + profile := pki.Spec.CertificateManagement.Custom.PKIProfile + return &profile, nil + default: + return nil, fmt.Errorf("unknown PKI certificate management mode: %q", pki.Spec.CertificateManagement.Mode) + } +} diff --git a/vendor/github.com/openshift/library-go/pkg/pki/resolve.go b/vendor/github.com/openshift/library-go/pkg/pki/resolve.go new file mode 100644 index 0000000000..1154fd48ea --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/pki/resolve.go @@ -0,0 +1,77 @@ +package pki + +import ( + "fmt" + + configv1alpha1 "github.com/openshift/api/config/v1alpha1" + "github.com/openshift/library-go/pkg/crypto" +) + +// CertificateConfig holds the resolved configuration for a specific certificate. +// Currently contains key configuration; will grow as the PKI API expands to +// include additional certificate properties. +type CertificateConfig struct { + // Key is the resolved key pair generator. + Key crypto.KeyPairGenerator +} + +// ResolveCertificateConfig resolves the effective certificate configuration +// for a given certificate type and name from the PKI profile. +// +// Returns nil if the provider returns a nil profile (Unmanaged mode), +// indicating that the caller should use its own default behavior. +// +// The name parameter is reserved for future per-certificate overrides and +// can be used for metrics and logging. +func ResolveCertificateConfig(provider PKIProfileProvider, certType CertificateType, name string) (*CertificateConfig, error) { + profile, err := provider.PKIProfile() + if err != nil { + return nil, fmt.Errorf("resolving PKI profile for %s certificate %q: %w", certType, name, err) + } + if profile == nil { + return nil, nil + } + + switch certType { + case CertificateTypeSigner: + return resolveKeyConfig(profile.Defaults, profile.SignerCertificates) + case CertificateTypeServing: + return resolveKeyConfig(profile.Defaults, profile.ServingCertificates) + case CertificateTypeClient: + return resolveKeyConfig(profile.Defaults, profile.ClientCertificates) + case CertificateTypePeer: + return resolvePeerKeyConfig(profile) + default: + return nil, fmt.Errorf("unknown certificate type: %q", certType) + } +} + +// resolveKeyConfig returns the override KeyConfig if its Algorithm is set, +// otherwise falls back to the default. +func resolveKeyConfig(defaults configv1alpha1.DefaultCertificateConfig, override configv1alpha1.CertificateConfig) (*CertificateConfig, error) { + apiKey := defaults.Key + if override.Key.Algorithm != "" { + apiKey = override.Key + } + g, err := KeyPairGeneratorFromAPI(apiKey) + if err != nil { + return nil, err + } + return &CertificateConfig{Key: g}, nil +} + +// resolvePeerKeyConfig resolves both the serving and client configs and +// returns whichever has higher NIST security strength. +func resolvePeerKeyConfig(profile *configv1alpha1.PKIProfile) (*CertificateConfig, error) { + servingCfg, err := resolveKeyConfig(profile.Defaults, profile.ServingCertificates) + if err != nil { + return nil, fmt.Errorf("resolving serving config for peer: %w", err) + } + clientCfg, err := resolveKeyConfig(profile.Defaults, profile.ClientCertificates) + if err != nil { + return nil, fmt.Errorf("resolving client config for peer: %w", err) + } + return &CertificateConfig{ + Key: strongerKeyPairGenerator(servingCfg.Key, clientCfg.Key), + }, nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/pki/types.go b/vendor/github.com/openshift/library-go/pkg/pki/types.go new file mode 100644 index 0000000000..2cf8282255 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/pki/types.go @@ -0,0 +1,23 @@ +package pki + +// CertificateType identifies the category of a certificate for profile resolution. +type CertificateType string + +const ( + // CertificateTypeSigner identifies certificate authority (CA) certificates + // that sign other certificates. + CertificateTypeSigner CertificateType = "signer" + + // CertificateTypeServing identifies TLS server certificates used to serve + // HTTPS endpoints. + CertificateTypeServing CertificateType = "serving" + + // CertificateTypeClient identifies client authentication certificates used + // to authenticate to servers. + CertificateTypeClient CertificateType = "client" + + // CertificateTypePeer identifies certificates used for both server and client + // authentication. The resolved key configuration is the stronger of the + // serving and client configurations. + CertificateTypePeer CertificateType = "peer" +) diff --git a/vendor/modules.txt b/vendor/modules.txt index b00d1fd514..d49f460658 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -388,7 +388,7 @@ github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/i github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/operatorcontrolplane github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/operatorcontrolplane/v1alpha1 github.com/openshift/client-go/operatorcontrolplane/listers/operatorcontrolplane/v1alpha1 -# github.com/openshift/library-go v0.0.0-20260303171201-5d9eb6295ff6 +# github.com/openshift/library-go v0.0.0-20260409165127-c57da2bf5720 ## explicit; go 1.25.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/authorization/hardcodedauthorizer @@ -422,6 +422,7 @@ github.com/openshift/library-go/pkg/operator/resource/retry github.com/openshift/library-go/pkg/operator/resourcesynccontroller github.com/openshift/library-go/pkg/operator/status github.com/openshift/library-go/pkg/operator/v1helpers +github.com/openshift/library-go/pkg/pki github.com/openshift/library-go/pkg/serviceability # github.com/openshift/machine-config-operator v0.0.1-0.20250724162154-ab14c8e2843b ## explicit; go 1.23.0