Skip to content
65 changes: 65 additions & 0 deletions sensor/kubernetes/localscanner/certificate_expiration.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, doesn't this code always return the first certificate found instead of returning the shorter expiration date?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renewalTimeInitialized is set to true the first time the renewalTime is set, so subsequent updates to renewalTime are only done if secretRenewalTime.Before(renewalTime). In any case I've added a test for GetSecretsCertRenewalTime that also checks this

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
}
82 changes: 82 additions & 0 deletions sensor/kubernetes/localscanner/certificate_expiration_test.go
Original file line number Diff line number Diff line change
@@ -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
}