diff --git a/central/localscanner/certificates.go b/central/localscanner/certificates.go new file mode 100644 index 0000000000000..4971aa77f9d8e --- /dev/null +++ b/central/localscanner/certificates.go @@ -0,0 +1,37 @@ +package localscanner + +import ( + "github.com/pkg/errors" + "github.com/stackrox/rox/generated/storage" + "github.com/stackrox/rox/pkg/certgen" + "github.com/stackrox/rox/pkg/mtls" +) + +// secretDataMap represents data stored as part of a secret. +type secretDataMap = map[string][]byte + +func generateServiceCertMap(serviceType storage.ServiceType, namespace string, clusterID string) (secretDataMap, error) { + if serviceType != storage.ServiceType_SCANNER_SERVICE && serviceType != storage.ServiceType_SCANNER_DB_SERVICE { + return nil, errors.Errorf("can only generate certificates for Scanner services, service type %s is not supported", + serviceType) + } + + ca, err := mtls.CAForSigning() + if err != nil { + return nil, errors.Wrap(err, "could not load CA for signing") + } + + numServiceCertDataEntries := 3 // cert pem + key pem + ca pem + fileMap := make(secretDataMap, numServiceCertDataEntries) + subject := mtls.NewSubject(clusterID, serviceType) + issueOpts := []mtls.IssueCertOption{ + mtls.WithValidityExpiringInDays(), + mtls.WithNamespace(namespace), + } + if err := certgen.IssueServiceCert(fileMap, ca, subject, "", issueOpts...); err != nil { + return nil, errors.Wrap(err, "error generating service certificate") + } + certgen.AddCACertToFileMap(fileMap, ca) + + return fileMap, nil +} diff --git a/central/localscanner/certificates_test.go b/central/localscanner/certificates_test.go new file mode 100644 index 0000000000000..69088fbddec8f --- /dev/null +++ b/central/localscanner/certificates_test.go @@ -0,0 +1,117 @@ +package localscanner + +import ( + "fmt" + "testing" + "time" + + "github.com/cloudflare/cfssl/helpers" + testutilsMTLS "github.com/stackrox/rox/central/testutils/mtls" + "github.com/stackrox/rox/generated/storage" + "github.com/stackrox/rox/pkg/certgen" + "github.com/stackrox/rox/pkg/mtls" + "github.com/stackrox/rox/pkg/testutils/envisolator" + "github.com/stretchr/testify/suite" +) + +const ( + namespace = "namespace" + clusterID = "clusterID" +) + +func TestHandler(t *testing.T) { + suite.Run(t, new(localScannerSuite)) +} + +type localScannerSuite struct { + suite.Suite + envIsolator *envisolator.EnvIsolator +} + +func (s *localScannerSuite) SetupSuite() { + s.envIsolator = envisolator.NewEnvIsolator(s.T()) +} + +func (s *localScannerSuite) TearDownTest() { + s.envIsolator.RestoreAll() +} + +func (s *localScannerSuite) SetupTest() { + err := testutilsMTLS.LoadTestMTLSCerts(s.envIsolator) + s.Require().NoError(err) +} + +func (s *localScannerSuite) TestCertMapContainsExpectedFiles() { + testCases := []struct { + service storage.ServiceType + expectError bool + }{ + {storage.ServiceType_SCANNER_SERVICE, false}, + {storage.ServiceType_SCANNER_DB_SERVICE, false}, + {storage.ServiceType_SENSOR_SERVICE, true}, + } + + for _, tc := range testCases { + certMap, err := generateServiceCertMap(tc.service, namespace, clusterID) + if tc.expectError { + s.Require().Error(err, tc.service) + continue + } else { + s.Require().NoError(err, tc.service) + } + expectedFiles := []string{"ca.pem", "cert.pem", "key.pem"} + s.Assert().Equal(len(expectedFiles), len(certMap)) + for _, key := range expectedFiles { + s.Assert().Contains(certMap, key, tc.service) + } + } +} + +func (s *localScannerSuite) TestValidateServiceCertificate() { + testCases := []storage.ServiceType{ + storage.ServiceType_SCANNER_SERVICE, + storage.ServiceType_SCANNER_DB_SERVICE, + } + + for _, serviceType := range testCases { + certMap, err := generateServiceCertMap(serviceType, namespace, clusterID) + s.Require().NoError(err, serviceType) + validatingCA, err := mtls.LoadCAForValidation(certMap["ca.pem"]) + s.Require().NoError(err, serviceType) + s.Assert().NoError(certgen.VerifyServiceCert(certMap, validatingCA, serviceType, ""), serviceType) + } +} + +func (s *localScannerSuite) TestCertificateGeneration() { + testCases := []struct { + service storage.ServiceType + expectOU string + expectedAlternativeNames []string + }{ + {storage.ServiceType_SCANNER_SERVICE, "SCANNER_SERVICE", + []string{"scanner.stackrox", "scanner.stackrox.svc", "scanner.namespace", "scanner.namespace.svc"}}, + {storage.ServiceType_SCANNER_DB_SERVICE, "SCANNER_DB_SERVICE", + []string{"scanner-db.stackrox", "scanner-db.stackrox.svc", "scanner-db.namespace", "scanner-db.namespace.svc"}}, + } + + for _, tc := range testCases { + certMap, err := generateServiceCertMap(tc.service, namespace, clusterID) + s.Require().NoError(err, tc.service) + cert, err := helpers.ParseCertificatePEM(certMap["cert.pem"]) + s.Require().NoError(err, tc.service) + + subject := cert.Subject + certOUs := subject.OrganizationalUnit + s.Assert().Equal(1, len(certOUs), tc.service) + s.Assert().Equal(tc.expectOU, certOUs[0], tc.service) + + s.Assert().Equal(fmt.Sprintf("%s: %s", tc.expectOU, clusterID), subject.CommonName, tc.service) + + certAlternativeNames := cert.DNSNames + s.Assert().Equal(len(tc.expectedAlternativeNames), len(certAlternativeNames), tc.service) + for _, name := range tc.expectedAlternativeNames { + s.Assert().Contains(certAlternativeNames, name, tc.service) + } + s.Assert().Equal(cert.NotBefore.Add(2*24*time.Hour), cert.NotAfter, tc.service) + } +} diff --git a/operator/pkg/central/extensions/reconcile_tls.go b/operator/pkg/central/extensions/reconcile_tls.go index 695cb2d7e0d33..2653c603643eb 100644 --- a/operator/pkg/central/extensions/reconcile_tls.go +++ b/operator/pkg/central/extensions/reconcile_tls.go @@ -180,7 +180,7 @@ func (r *createCentralTLSExtensionRun) generateInitBundleTLSData(fileNamePrefix fileMap := make(secretDataMap, numServiceCertDataEntries) bundleID := uuid.NewV4() subject := mtls.NewInitSubject(centralsensor.EphemeralInitCertClusterID, serviceType, bundleID) - if err := r.generateServiceTLSData(subject, fileNamePrefix, fileMap, mtls.WithEphemeralValidity()); err != nil { + if err := r.generateServiceTLSData(subject, fileNamePrefix, fileMap, mtls.WithValidityExpiringInHours()); err != nil { return nil, err } return fileMap, nil diff --git a/pkg/mtls/ca_test.go b/pkg/mtls/ca_test.go index dfbd16fe475c6..b12aa4a37fe3b 100644 --- a/pkg/mtls/ca_test.go +++ b/pkg/mtls/ca_test.go @@ -21,11 +21,16 @@ func Test_CA_IssueCertForSubject(t *testing.T) { minNotAfter: 364 * 24 * time.Hour, maxNotAfter: 366 * 24 * time.Hour, }, - "ephemeral cert": { - opts: []IssueCertOption{WithEphemeralValidity()}, + "ephemeral cert hourly expiration": { + opts: []IssueCertOption{WithValidityExpiringInHours()}, minNotAfter: 2 * time.Hour, maxNotAfter: 4 * time.Hour, }, + "ephemeral cert daily expiration": { + opts: []IssueCertOption{WithValidityExpiringInDays()}, + minNotAfter: (2*24 - 1) * time.Hour, + maxNotAfter: (2*24 + 1) * time.Hour, + }, } cert, _, key, err := initca.New(&csr.CertificateRequest{ diff --git a/pkg/mtls/crypto.go b/pkg/mtls/crypto.go index 3fc2b06abe9f9..03ff11ba901aa 100644 --- a/pkg/mtls/crypto.go +++ b/pkg/mtls/crypto.go @@ -55,8 +55,11 @@ const ( certLifetime = 365 * 24 * time.Hour - ephemeralProfile = "ephemeral" - ephemeralInitBundleCertLifetime = 3 * time.Hour + ephemeralProfileWithExpirationInHours = "ephemeralWithExpirationInHours" + ephemeralProfileWithExpirationInHoursCertLifetime = 3 * time.Hour + + ephemeralProfileWithExpirationInDays = "ephemeralWithExpirationInDays" + ephemeralProfileWithExpirationInDaysCertLifetime = 2 * 24 * time.Hour ) var ( @@ -177,6 +180,21 @@ func CACert() (*x509.Certificate, []byte, error) { return caCert, caCertDER, caCertErr } +// CAForSigning reads the cert and key from the local file system and returns +// a corresponding CA instance that can be used for signing. +func CAForSigning() (CA, error) { + _, certPEM, _, err := readCA() + if err != nil { + return nil, errors.Wrap(err, "could not read CA cert file") + } + keyPEM, err := readCAKey() + if err != nil { + return nil, errors.Wrap(err, "could not read CA key file") + } + + return LoadCAForSigning(certPEM, keyPEM) +} + func signer() (cfsigner.Signer, error) { return local.NewSignerFromFile(caFilePathSetting.Setting(), caKeyFilePathSetting.Setting(), createSigningPolicy()) } @@ -185,7 +203,8 @@ func createSigningPolicy() *config.Signing { return &config.Signing{ Default: createSigningProfile(certLifetime, beforeGracePeriod), Profiles: map[string]*config.SigningProfile{ - ephemeralProfile: createSigningProfile(ephemeralInitBundleCertLifetime, 0), + ephemeralProfileWithExpirationInHours: createSigningProfile(ephemeralProfileWithExpirationInHoursCertLifetime, 0), + ephemeralProfileWithExpirationInDays: createSigningProfile(ephemeralProfileWithExpirationInDaysCertLifetime, 0), }, } } diff --git a/pkg/mtls/issue_options.go b/pkg/mtls/issue_options.go index 8c430dd2f1181..2666d1b42e3a8 100644 --- a/pkg/mtls/issue_options.go +++ b/pkg/mtls/issue_options.go @@ -21,10 +21,17 @@ func WithNamespace(namespace string) IssueCertOption { } } -// WithEphemeralValidity requests certificates with short validity. +// WithValidityExpiringInHours requests certificates with validity expiring in the order of hours. // This option is suitable for issuing init bundles which cannot be revoked. -func WithEphemeralValidity() IssueCertOption { +func WithValidityExpiringInHours() IssueCertOption { return func(o *issueOptions) { - o.signerProfile = ephemeralProfile + o.signerProfile = ephemeralProfileWithExpirationInHours + } +} + +// WithValidityExpiringInDays requests certificates with validity expiring in the order of days. +func WithValidityExpiringInDays() IssueCertOption { + return func(o *issueOptions) { + o.signerProfile = ephemeralProfileWithExpirationInDays } }