Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions central/localscanner/certificates.go
Original file line number Diff line number Diff line change
@@ -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
}
117 changes: 117 additions & 0 deletions central/localscanner/certificates_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion operator/pkg/central/extensions/reconcile_tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions pkg/mtls/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
25 changes: 22 additions & 3 deletions pkg/mtls/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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())
}
Expand All @@ -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),
},
}
}
Expand Down
13 changes: 10 additions & 3 deletions pkg/mtls/issue_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}