-
Notifications
You must be signed in to change notification settings - Fork 552
[OCPBUGS-25341]: perform operator apiService certificate validity checks directly #3217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ import ( | |
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/util/intstr" | ||
"k8s.io/apimachinery/pkg/util/sets" | ||
corev1ac "k8s.io/client-go/applyconfigurations/core/v1" | ||
rbacv1ac "k8s.io/client-go/applyconfigurations/rbac/v1" | ||
|
||
|
@@ -157,6 +158,14 @@ func ServiceName(deploymentName string) string { | |
return deploymentName + "-service" | ||
} | ||
|
||
func HostnamesForService(serviceName, namespace string) []string { | ||
return []string{ | ||
fmt.Sprintf("%s.%s", serviceName, namespace), | ||
fmt.Sprintf("%s.%s.svc", serviceName, namespace), | ||
fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, namespace), | ||
} | ||
} | ||
|
||
func (i *StrategyDeploymentInstaller) getCertResources() []certResource { | ||
return append(i.apiServiceDescriptions, i.webhookDescriptions...) | ||
} | ||
|
@@ -223,15 +232,74 @@ func (i *StrategyDeploymentInstaller) CertsRotated() bool { | |
return i.certificatesRotated | ||
} | ||
|
||
func ShouldRotateCerts(csv *v1alpha1.ClusterServiceVersion) bool { | ||
// shouldRotateCerts indicates whether an apiService cert should be rotated due to being | ||
// malformed, invalid, expired, inactive or within a specific freshness interval (DefaultCertMinFresh) before expiry. | ||
func shouldRotateCerts(certSecret *corev1.Secret, hosts []string) bool { | ||
now := metav1.Now() | ||
if !csv.Status.CertsRotateAt.IsZero() && csv.Status.CertsRotateAt.Before(&now) { | ||
caPEM, ok := certSecret.Data[OLMCAPEMKey] | ||
if !ok { | ||
// missing CA cert in secret | ||
return true | ||
} | ||
certPEM, ok := certSecret.Data["tls.crt"] | ||
if !ok { | ||
// missing cert in secret | ||
return true | ||
} | ||
|
||
ca, err := certs.PEMToCert(caPEM) | ||
if err != nil { | ||
// malformed CA cert | ||
return true | ||
} | ||
cert, err := certs.PEMToCert(certPEM) | ||
if err != nil { | ||
// malformed cert | ||
return true | ||
} | ||
|
||
// check for cert freshness | ||
if !certs.Active(ca) || now.Time.After(CalculateCertRotatesAt(ca.NotAfter)) || | ||
!certs.Active(cert) || now.Time.After(CalculateCertRotatesAt(cert.NotAfter)) { | ||
return true | ||
} | ||
|
||
// Check validity of serving cert and if serving cert is trusted by the CA | ||
for _, host := range hosts { | ||
if err := certs.VerifyCert(ca, cert, host); err != nil { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func (i *StrategyDeploymentInstaller) ShouldRotateCerts(s Strategy) (bool, error) { | ||
joelanford marked this conversation as resolved.
Show resolved
Hide resolved
|
||
strategy, ok := s.(*v1alpha1.StrategyDetailsDeployment) | ||
if !ok { | ||
return false, fmt.Errorf("failed to install %s strategy with deployment installer: unsupported deployment install strategy", strategy.GetStrategyName()) | ||
} | ||
|
||
hasCerts := sets.New[string]() | ||
for _, c := range i.getCertResources() { | ||
hasCerts.Insert(c.getDeploymentName()) | ||
} | ||
for _, sddSpec := range strategy.DeploymentSpecs { | ||
if hasCerts.Has(sddSpec.Name) { | ||
certSecret, err := i.strategyClient.GetOpLister().CoreV1().SecretLister().Secrets(i.owner.GetNamespace()).Get(SecretName(ServiceName(sddSpec.Name))) | ||
if err == nil { | ||
if shouldRotateCerts(certSecret, HostnamesForService(ServiceName(sddSpec.Name), i.owner.GetNamespace())) { | ||
return true, nil | ||
} | ||
} else if apierrors.IsNotFound(err) { | ||
return true, nil | ||
} else { | ||
return false, err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's some other error, what happens? This does not rotate the certificate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume it means we enter exponential backoff (at least until we hit the 5m install plan timeout)? Any other error is likely to be either temporary (in which case it's hopefully resolved next time around), or not (in which case we would likely fail in a similar way trying to rotate the cert). The only scenario that is coming to mind where |
||
} | ||
} | ||
} | ||
return false, nil | ||
} | ||
|
||
func CalculateCertExpiration(startingFrom time.Time) time.Time { | ||
return startingFrom.Add(DefaultCertValidFor) | ||
} | ||
|
@@ -267,10 +335,7 @@ func (i *StrategyDeploymentInstaller) installCertRequirementsForDeployment(deplo | |
} | ||
|
||
// Create signed serving cert | ||
hosts := []string{ | ||
fmt.Sprintf("%s.%s", serviceName, i.owner.GetNamespace()), | ||
fmt.Sprintf("%s.%s.svc", serviceName, i.owner.GetNamespace()), | ||
} | ||
hosts := HostnamesForService(serviceName, i.owner.GetNamespace()) | ||
servingPair, err := certGenerator.Generate(expiration, Organization, ca, hosts) | ||
if err != nil { | ||
logger.Warnf("could not generate signed certs for hosts %v", hosts) | ||
|
@@ -314,10 +379,10 @@ func (i *StrategyDeploymentInstaller) installCertRequirementsForDeployment(deplo | |
|
||
// Attempt an update | ||
// TODO: Check that the secret was not modified | ||
if existingCAPEM, ok := existingSecret.Data[OLMCAPEMKey]; ok && !ShouldRotateCerts(i.owner.(*v1alpha1.ClusterServiceVersion)) { | ||
if !shouldRotateCerts(existingSecret, HostnamesForService(serviceName, i.owner.GetNamespace())) { | ||
logger.Warnf("reusing existing cert %s", secret.GetName()) | ||
secret = existingSecret | ||
caPEM = existingCAPEM | ||
caPEM = existingSecret.Data[OLMCAPEMKey] | ||
caHash = certs.PEMSHA256(caPEM) | ||
} else { | ||
if _, err := i.strategyClient.GetOpClient().UpdateSecret(secret); err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -93,17 +93,12 @@ func (a *Operator) checkAPIServiceResources(csv *v1alpha1.ClusterServiceVersion, | |
|
||
// Check if CA is Active | ||
caBundle := apiService.Spec.CABundle | ||
ca, err := certs.PEMToCert(caBundle) | ||
_, err = certs.PEMToCert(caBundle) | ||
if err != nil { | ||
logger.Warnf("could not convert APIService CA bundle to x509 cert") | ||
errs = append(errs, err) | ||
continue | ||
} | ||
if !certs.Active(ca) { | ||
logger.Warnf("CA cert not active") | ||
errs = append(errs, fmt.Errorf("found the CA cert is not active")) | ||
continue | ||
} | ||
Comment on lines
-102
to
-106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're no longer checking the certs (here and below)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've combined the cert checks with freshness immediately after the APIService checks for successful/failed CSVs. We don't necessarily need to repeat the checks here again. |
||
|
||
// Check if serving cert is active | ||
secretName := install.SecretName(serviceName) | ||
|
@@ -113,17 +108,12 @@ func (a *Operator) checkAPIServiceResources(csv *v1alpha1.ClusterServiceVersion, | |
errs = append(errs, err) | ||
continue | ||
} | ||
cert, err := certs.PEMToCert(secret.Data["tls.crt"]) | ||
_, err = certs.PEMToCert(secret.Data["tls.crt"]) | ||
if err != nil { | ||
logger.Warnf("could not convert serving cert to x509 cert") | ||
errs = append(errs, err) | ||
continue | ||
} | ||
if !certs.Active(cert) { | ||
logger.Warnf("serving cert not active") | ||
errs = append(errs, fmt.Errorf("found the serving cert not active")) | ||
continue | ||
} | ||
|
||
// Check if CA hash matches expected | ||
caHash := hashFunc(caBundle) | ||
|
@@ -133,18 +123,6 @@ func (a *Operator) checkAPIServiceResources(csv *v1alpha1.ClusterServiceVersion, | |
continue | ||
} | ||
|
||
// Check if serving cert is trusted by the CA | ||
hosts := []string{ | ||
fmt.Sprintf("%s.%s", service.GetName(), csv.GetNamespace()), | ||
fmt.Sprintf("%s.%s.svc", service.GetName(), csv.GetNamespace()), | ||
} | ||
for _, host := range hosts { | ||
if err := certs.VerifyCert(ca, cert, host); err != nil { | ||
errs = append(errs, fmt.Errorf("could not verify cert: %s", err.Error())) | ||
continue | ||
} | ||
} | ||
|
||
// Ensure the existing Deployment has a matching CA hash annotation | ||
deployment, err := a.lister.AppsV1().DeploymentLister().Deployments(csv.GetNamespace()).Get(desc.DeploymentName) | ||
if apierrors.IsNotFound(err) || err != nil { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not adding
.cluster
nor.cluster.local
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should resolve even without the cluster domain specified, though there shouldn't be any harm in adding the fqdns for the default cluster domains. We could possibly also lookup the actual domain similar to pkg/package-server/client/util.go, though that may not be necessary