Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f9dbf7b
Initial code for central service to generate local scanner certificates
Dec 28, 2021
e19fc2e
fix style issues
Dec 28, 2021
1d5fda9
Add unit test for LocalScannerService
Jan 3, 2022
c3b0959
Simplify IssueLocalScannerCerts
Jan 4, 2022
71d7f13
remove redundant field
Jan 4, 2022
2a087bd
Infer cluster id from request context
Jan 4, 2022
7a35476
Reorder func to have entry point on top, and aux funcs dowmn
Jan 5, 2022
ff0aee4
Memoize CAForSigning
Jan 5, 2022
d1b7d62
fix code style
Jan 5, 2022
3e23314
Number proto message fields starting in 1
Jan 5, 2022
b1d908e
Properly infer cluster id from request context
Jan 5, 2022
225f4db
Replace new gRPC service with new messages in SensorService.Communicate
Jan 11, 2022
2057450
fix checkstyle
Jan 12, 2022
1e09c1f
fix typo and error message
Jan 13, 2022
bbde9a3
protect IssueLocalScannerCerts with feature flag LocalImageScanning
Jan 13, 2022
c3c97b3
enable IssueLocalScannerCerts feature flag for tests
Jan 13, 2022
6951eb8
Return a failure message on local certificate issue error
Jan 13, 2022
b88111d
Add test for processIssueLocalScannerCertsRequest
Jan 13, 2022
83e208c
Skip tests when feature flag dependency is disabled
Jan 13, 2022
8921d85
Use features.LocalImageScanning always directly
Jan 14, 2022
4fe13ce
Quote namespace in error log
Jan 14, 2022
1f5bd75
Use assertion methods directly insted of through s.Assert()
Jan 14, 2022
1a1b1fd
Make sure the result of handleMessage is always checked
Jan 14, 2022
7bfbd6c
Add format to proto field names for certs and keys
Jan 14, 2022
a2b5cef
get namespace from sensor hello insteadof request parameter
Jan 14, 2022
a771af3
Avoid redundancies and generalize proto messages
Jan 14, 2022
40a6fa0
Add request id for pairing responses with their requests
Jan 14, 2022
b1703fe
Use Len assertion instead of Equal of `len`
Jan 14, 2022
2e3b639
Use require to avoid panic later on in test
Jan 14, 2022
06e8eaa
use proper list comparison instead of a loop and len check
Jan 14, 2022
98bb61b
use subtest for all test tables
Jan 14, 2022
7356d0e
use require instead of assert to prevent potential test panic
Jan 14, 2022
d578d40
Draft of local scanner certificate refresh
Jan 11, 2022
62efeb6
fix style
Jan 12, 2022
e243eac
Bug fixes
Jan 12, 2022
4148880
checkstyle
Jan 12, 2022
1d5355f
adapt client to proto changes
Jan 14, 2022
b60b336
add FIXME for handling request id
Jan 14, 2022
a34c515
Remove unnecessary intermediate variable
Jan 14, 2022
1a721db
check cert bytes are not empty before parsing them
Jan 14, 2022
58941a1
rename aux func, and lower log level
Jan 14, 2022
f07bcba
Use "TLSIssuer" instead of "operator"
Jan 14, 2022
b298a2d
fix typo in comment
Jan 14, 2022
0985ef8
launch local scanner TLS issue only if feature flag activated
Jan 14, 2022
abefd5c
Use nil casting trick to typecheck SensorComponent implementation
Jan 17, 2022
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
49 changes: 49 additions & 0 deletions central/localscanner/certificates.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,64 @@
package localscanner

import (
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/certgen"
"github.com/stackrox/rox/pkg/features"
"github.com/stackrox/rox/pkg/mtls"
)

// secretDataMap represents data stored as part of a secret.
type secretDataMap = map[string][]byte

// IssueLocalScannerCerts issue certificates for a local scanner running in secured clusters.
func IssueLocalScannerCerts(namespace string, clusterID string) (*storage.TypedServiceCertificateSet, error) {
if !features.LocalImageScanning.Enabled() {
return nil, errors.Errorf("feature '%s' is disabled", features.LocalImageScanning.Name())
}
if namespace == "" {
return nil, errors.New("namespace is required to issue the certificates for the local scanner")
}

var certIssueError error
caPem, scannerCertificate, err := localScannerCertificatesFor(storage.ServiceType_SCANNER_SERVICE, namespace, clusterID)
if err != nil {
certIssueError = multierror.Append(certIssueError, err)
}
_, scannerDBCertificate, err := localScannerCertificatesFor(storage.ServiceType_SCANNER_DB_SERVICE, namespace, clusterID)
if err != nil {
certIssueError = multierror.Append(certIssueError, err)
}
if certIssueError != nil {
return nil, certIssueError
}

return &storage.TypedServiceCertificateSet{
CaPem: caPem,
ServiceCerts: []*storage.TypedServiceCertificate{
scannerCertificate,
scannerDBCertificate,
},
}, nil
}

func localScannerCertificatesFor(serviceType storage.ServiceType, namespace string, clusterID string) (caPem []byte, cert *storage.TypedServiceCertificate, err error) {
certificates, err := generateServiceCertMap(serviceType, namespace, clusterID)
if err != nil {
return nil, nil, errors.Wrapf(err, "generating certificate for service %s", serviceType)
}
caPem = certificates[mtls.CACertFileName]
cert = &storage.TypedServiceCertificate{
ServiceType: serviceType,
Cert: &storage.ServiceCertificate{
CertPem: certificates[mtls.ServiceCertFileName],
KeyPem: certificates[mtls.ServiceKeyFileName],
},
}
return
}

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",
Expand Down
118 changes: 83 additions & 35 deletions central/localscanner/certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
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/features"
"github.com/stackrox/rox/pkg/mtls"
"github.com/stackrox/rox/pkg/testutils/envisolator"
"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -52,18 +53,19 @@ func (s *localScannerSuite) TestCertMapContainsExpectedFiles() {
}

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)
}
s.Run(tc.service.String(), func() {
certMap, err := generateServiceCertMap(tc.service, namespace, clusterID)
if tc.expectError {
s.Require().Error(err)
return
}
s.Require().NoError(err)
expectedFiles := []string{"ca.pem", "cert.pem", "key.pem"}
s.Len(certMap, len(expectedFiles))
for _, key := range expectedFiles {
s.Contains(certMap, key)
}
})
}
}

Expand All @@ -74,11 +76,13 @@ func (s *localScannerSuite) TestValidateServiceCertificate() {
}

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)
s.Run(serviceType.String(), func() {
certMap, err := generateServiceCertMap(serviceType, namespace, clusterID)
s.Require().NoError(err)
validatingCA, err := mtls.LoadCAForValidation(certMap["ca.pem"])
s.Require().NoError(err)
s.NoError(certgen.VerifyServiceCert(certMap, validatingCA, serviceType, ""))
})
}
}

Expand All @@ -95,23 +99,67 @@ func (s *localScannerSuite) TestCertificateGeneration() {
}

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)
s.Run(tc.service.String(), func() {
certMap, err := generateServiceCertMap(tc.service, namespace, clusterID)
s.Require().NoError(err)
cert, err := helpers.ParseCertificatePEM(certMap["cert.pem"])
s.Require().NoError(err)

subject := cert.Subject
certOUs := subject.OrganizationalUnit
s.Require().Len(certOUs, 1)
s.Equal(tc.expectOU, certOUs[0])

s.Equal(fmt.Sprintf("%s: %s", tc.expectOU, clusterID), subject.CommonName)

certAlternativeNames := cert.DNSNames
s.ElementsMatch(tc.expectedAlternativeNames, certAlternativeNames)
s.Equal(cert.NotBefore.Add(2*24*time.Hour), cert.NotAfter)
})
}
}

func (s *localScannerSuite) TestServiceIssueLocalScannerCertsFeatureFlagDisabled() {
s.envIsolator.Setenv(features.LocalImageScanning.EnvVar(), "false")
if features.LocalImageScanning.Enabled() {
s.T().Skip()
}

_, err := IssueLocalScannerCerts(namespace, clusterID)

s.Error(err)
}

func (s *localScannerSuite) TestServiceIssueLocalScannerCerts() {
s.envIsolator.Setenv(features.LocalImageScanning.EnvVar(), "true")
if !features.LocalImageScanning.Enabled() {
s.T().Skip()
}
testCases := map[string]struct {
namespace string
clusterID string
shouldFail bool
}{
"no parameter missing": {namespace: namespace, clusterID: clusterID, shouldFail: false},
"namespace missing": {namespace: "", clusterID: clusterID, shouldFail: true},
"clusterID missing": {namespace: namespace, clusterID: "", shouldFail: true},
}
for tcName, tc := range testCases {
s.Run(tcName, func() {
certs, err := IssueLocalScannerCerts(tc.namespace, tc.clusterID)
if tc.shouldFail {
s.Require().Error(err)
return
}
s.Require().NoError(err)
s.Require().NotNil(certs.GetCaPem())
s.Require().NotEmpty(certs.GetServiceCerts())
for _, cert := range certs.ServiceCerts {
s.Contains([]storage.ServiceType{storage.ServiceType_SCANNER_SERVICE,
storage.ServiceType_SCANNER_DB_SERVICE}, cert.GetServiceType())
s.NotEmpty(cert.GetCert().GetCertPem())
s.NotEmpty(cert.GetCert().GetKeyPem())
}
})
}
}
45 changes: 45 additions & 0 deletions central/sensor/service/connection/connection_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package connection

import (
"context"
"fmt"

"github.com/pkg/errors"
"github.com/stackrox/rox/central/localscanner"
"github.com/stackrox/rox/central/networkpolicies/graph"
"github.com/stackrox/rox/central/scrape"
"github.com/stackrox/rox/central/sensor/networkentities"
Expand Down Expand Up @@ -215,6 +217,8 @@ func (c *sensorConnection) handleMessage(ctx context.Context, msg *central.MsgFr
return c.networkPoliciesCtrl.ProcessNetworkPoliciesResponse(m.NetworkPoliciesResponse)
case *central.MsgFromSensor_TelemetryDataResponse:
return c.telemetryCtrl.ProcessTelemetryDataResponse(m.TelemetryDataResponse)
case *central.MsgFromSensor_IssueLocalScannerCertsRequest:
return c.processIssueLocalScannerCertsRequest(ctx, m.IssueLocalScannerCertsRequest)
case *central.MsgFromSensor_Event:
// Special case the reprocess deployment because its fields are already set
if msg.GetEvent().GetReprocessDeployment() != nil {
Expand All @@ -234,6 +238,47 @@ func (c *sensorConnection) handleMessage(ctx context.Context, msg *central.MsgFr
return c.eventPipeline.Run(ctx, msg, c)
}

func (c *sensorConnection) processIssueLocalScannerCertsRequest(ctx context.Context, request *central.IssueLocalScannerCertsRequest) error {
requestID := request.GetRequestId()
clusterID := c.clusterID
namespace := c.sensorHello.GetDeploymentIdentification().GetAppNamespace()
errMsg := fmt.Sprintf("issuing local Scanner certificates for request ID %q, cluster ID %q and namespace %q",
requestID, clusterID, namespace)
var (
err error
response *central.IssueLocalScannerCertsResponse
)
if requestID == "" {
err = errors.New("requestID is required to issue the certificates for the local scanner")
} else {
certificates, issueErr := localscanner.IssueLocalScannerCerts(namespace, clusterID)
err = issueErr
response = &central.IssueLocalScannerCertsResponse{
RequestId: requestID,
Response: &central.IssueLocalScannerCertsResponse_Certificates{
Certificates: certificates,
},
}
}
if err != nil {
response = &central.IssueLocalScannerCertsResponse{
RequestId: requestID,
Response: &central.IssueLocalScannerCertsResponse_Error{
Error: &central.LocalScannerCertsIssueError{
Message: fmt.Sprintf("%s: %s", errMsg, err.Error()),
},
},
}
}
err = c.InjectMessage(ctx, &central.MsgToSensor{
Msg: &central.MsgToSensor_IssueLocalScannerCertsResponse{IssueLocalScannerCertsResponse: response},
})
if err != nil {
return errors.Wrap(err, errMsg)
}
return nil
}

// getPolicySyncMsg fetches stored policies and prepares them for delivery to sensor.
func (c *sensorConnection) getPolicySyncMsg(ctx context.Context) (*central.MsgToSensor, error) {
policies, err := c.policyMgr.GetAllPolicies(ctx)
Expand Down
Loading