diff --git a/sensor/kubernetes/localscanner/certificate_expiration.go b/sensor/kubernetes/localscanner/certificate_expiration.go new file mode 100644 index 0000000000000..446266fea3455 --- /dev/null +++ b/sensor/kubernetes/localscanner/certificate_expiration.go @@ -0,0 +1,65 @@ +package localscanner + +import ( + "crypto/x509" + "math/rand" + "time" + + "github.com/cloudflare/cfssl/helpers" + "github.com/pkg/errors" + "github.com/stackrox/rox/generated/storage" +) + +var ( + // ErrEmptyCertificate indicates that the certificate stored in a secret is empty. + ErrEmptyCertificate = errors.New("empty certificate") +) + +// GetCertsRenewalTime computes the time when the service certificates should be refreshed. +// If different services have different expiration times then the earliest time is returned. +func GetCertsRenewalTime(certificates *storage.TypedServiceCertificateSet) (time.Time, error) { + var ( + renewalTime time.Time + renewalTimeInitialized bool + ) + for _, certificate := range certificates.GetServiceCerts() { + certRenewalTime, err := getCertificateRenewalTime(certificate) + if err != nil { + return renewalTime, err + } + if !renewalTimeInitialized || certRenewalTime.Before(renewalTime) { + renewalTimeInitialized = true + renewalTime = certRenewalTime + } + } + return renewalTime, nil +} + +func getCertificateRenewalTime(certificate *storage.TypedServiceCertificate) (time.Time, error) { + certBytes := certificate.GetCert().GetCertPem() + var ( + cert *x509.Certificate + err error + ) + if len(certBytes) == 0 { + err = ErrEmptyCertificate + } else { + cert, err = helpers.ParseCertificatePEM(certBytes) + } + if err != nil { + var zeroTime time.Time + return zeroTime, err + } + + return calculateRenewalTime(cert), nil +} + +// In order to ensure certificates are rotated before expiration, this returns a renewal time no later than +// half its expiration date. +func calculateRenewalTime(cert *x509.Certificate) time.Time { + certValidityDurationSecs := cert.NotAfter.Sub(cert.NotBefore).Seconds() + durationBeforeRenewalAttempt := time.Second * + (time.Duration(certValidityDurationSecs/2) - time.Duration(rand.Intn(int(certValidityDurationSecs/10)))) + certRenewalTime := cert.NotBefore.Add(durationBeforeRenewalAttempt) + return certRenewalTime +} diff --git a/sensor/kubernetes/localscanner/certificate_expiration_test.go b/sensor/kubernetes/localscanner/certificate_expiration_test.go new file mode 100644 index 0000000000000..3d0a9365ca795 --- /dev/null +++ b/sensor/kubernetes/localscanner/certificate_expiration_test.go @@ -0,0 +1,82 @@ +package localscanner + +import ( + "testing" + "time" + + "github.com/stackrox/rox/generated/storage" + "github.com/stackrox/rox/pkg/mtls" + testutilsMTLS "github.com/stackrox/rox/pkg/mtls/testutils" + "github.com/stackrox/rox/pkg/testutils/envisolator" + "github.com/stretchr/testify/suite" +) + +var ( + // should be the same as the expiration corresponding to `mtls.WithValidityExpiringInHours()`. + afterOffset = 3 * time.Hour +) + +func TestGetSecretRenewalTime(t *testing.T) { + suite.Run(t, new(getSecretRenewalTimeSuite)) +} + +type getSecretRenewalTimeSuite struct { + suite.Suite + envIsolator *envisolator.EnvIsolator +} + +func (s *getSecretRenewalTimeSuite) SetupSuite() { + s.envIsolator = envisolator.NewEnvIsolator(s.T()) +} + +func (s *getSecretRenewalTimeSuite) SetupTest() { + err := testutilsMTLS.LoadTestMTLSCerts(s.envIsolator) + s.Require().NoError(err) +} + +func (s *getSecretRenewalTimeSuite) TearDownTest() { + s.envIsolator.RestoreAll() +} + +func (s *getSecretRenewalTimeSuite) TestGetSecretsCertRenewalTime() { + certPEMHours, err := issueCertificatePEM(mtls.WithValidityExpiringInHours()) + s.Require().NoError(err) + certPEMDays, err := issueCertificatePEM(mtls.WithValidityExpiringInDays()) + s.Require().NoError(err) + certificates := &storage.TypedServiceCertificateSet{ + CaPem: make([]byte, 0), + ServiceCerts: []*storage.TypedServiceCertificate{ + { + ServiceType: storage.ServiceType_SCANNER_SERVICE, + Cert: &storage.ServiceCertificate{ + CertPem: certPEMHours, + }, + }, + { + ServiceType: storage.ServiceType_SCANNER_DB_SERVICE, + Cert: &storage.ServiceCertificate{ + CertPem: certPEMDays, + }, + }, + }, + } + + certRenewalTime, err := GetCertsRenewalTime(certificates) + + s.Require().NoError(err) + certDuration := time.Until(certRenewalTime) + s.LessOrEqual(certDuration, afterOffset/2) +} + +func issueCertificatePEM(issueOption mtls.IssueCertOption) ([]byte, error) { + ca, err := mtls.CAForSigning() + if err != nil { + return nil, err + } + subject := mtls.NewSubject("clusterId", storage.ServiceType_SCANNER_SERVICE) + cert, err := ca.IssueCertForSubject(subject, issueOption) + if err != nil { + return nil, err + } + return cert.CertPEM, err +}