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
17 changes: 11 additions & 6 deletions central/notifier/service/service_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ var (
})
)

const errSecureNotifierString = "Error securing notifier"

// ClusterService is the struct that manages the cluster API
type serviceImpl struct {
v1.UnimplementedNotifierServiceServer
Expand Down Expand Up @@ -145,8 +147,9 @@ func (s *serviceImpl) UpdateNotifier(ctx context.Context, request *v1.UpdateNoti
if request.GetUpdatePassword() {
err := notifierUtils.SecureNotifier(request.GetNotifier(), s.cryptoKey)
if err != nil {
// Don't send out error from crypto lib
return nil, errors.New("Error securing notifier")
// Don't send out error from crypto lib but log it.
log.Errorf("%s: %s", errSecureNotifierString, err.Error())
return nil, errors.New(errSecureNotifierString)
}
}
notifier, err := notifierCreator(request.GetNotifier())
Expand Down Expand Up @@ -174,8 +177,9 @@ func (s *serviceImpl) PostNotifier(ctx context.Context, request *storage.Notifie
}
err := notifierUtils.SecureNotifier(request, s.cryptoKey)
if err != nil {
// Don't send out error from crypto lib
return nil, errors.New("Error securing notifier")
// Don't send out error from crypto lib but log it.
log.Errorf("%s: %s", errSecureNotifierString, err.Error())
return nil, errors.New(errSecureNotifierString)
}
notifier, err := pkgNotifiers.CreateNotifier(request)
if err != nil {
Expand Down Expand Up @@ -213,8 +217,9 @@ func (s *serviceImpl) TestUpdatedNotifier(ctx context.Context, request *v1.Updat
if request.GetUpdatePassword() {
err := notifierUtils.SecureNotifier(request.GetNotifier(), s.cryptoKey)
if err != nil {
// Don't send out error from crypto lib
return nil, errors.New("Error securing notifier")
// Don't send out error from crypto lib but log it.
log.Errorf("%s: %s", errSecureNotifierString, err.Error())
return nil, errors.New(errSecureNotifierString)
}
}
notifier, err := pkgNotifiers.CreateNotifier(request.GetNotifier())
Expand Down
47 changes: 41 additions & 6 deletions central/notifiers/microsoftsentinel/sentinel.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/monitor/ingestion/azlogs"
"github.com/pkg/errors"
notifierUtils "github.com/stackrox/rox/central/notifiers/utils"
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/administration/events/codes"
"github.com/stackrox/rox/pkg/administration/events/option"
"github.com/stackrox/rox/pkg/cryptoutils/cryptocodec"
"github.com/stackrox/rox/pkg/env"
"github.com/stackrox/rox/pkg/errorhelpers"
"github.com/stackrox/rox/pkg/features"
"github.com/stackrox/rox/pkg/logging"
"github.com/stackrox/rox/pkg/notifiers"
"github.com/stackrox/rox/pkg/protocompat"
"github.com/stackrox/rox/pkg/retry"
"github.com/stackrox/rox/pkg/utils"
"github.com/stackrox/rox/pkg/uuid"
"github.com/stackrox/rox/pkg/x509utils"
"google.golang.org/protobuf/proto"
Expand All @@ -36,9 +40,18 @@ var (

func init() {
if features.MicrosoftSentinelNotifier.Enabled() {
cryptoKey := ""
var err error
if env.EncNotifierCreds.BooleanSetting() {
cryptoKey, _, err = notifierUtils.GetActiveNotifierEncryptionKey()
if err != nil {
utils.Should(errors.Wrap(err, "Error reading encryption key, notifier will be unable to send notifications"))
}
}

log.Debug("Microsoft Sentinel notifier enabled.")
notifiers.Add(notifiers.MicrosoftSentinelType, func(notifier *storage.Notifier) (notifiers.Notifier, error) {
return newSentinelNotifier(notifier)
return newSentinelNotifier(notifier, cryptocodec.Singleton(), cryptoKey)
})
}
}
Expand Down Expand Up @@ -69,25 +82,46 @@ func (s sentinel) AuditLoggingEnabled() bool {
}

// newSentinelNotifier returns a new sentinel notifier.
func newSentinelNotifier(notifier *storage.Notifier) (*sentinel, error) {
func newSentinelNotifier(notifier *storage.Notifier, cryptoCodec cryptocodec.CryptoCodec, key string) (*sentinel, error) {
config := notifier.GetMicrosoftSentinel()

err := Validate(config, false)
if err != nil {
return nil, errors.Wrap(err, "could not create sentinel notifier, validation failed")
}

secret, err := notifierUtils.GetCredentials(notifier)
if err != nil {
return nil, errors.Wrapf(err, "Could not read secret for notifier %q", notifier.GetName())
}

if env.EncNotifierCreds.BooleanSetting() {
// Decrypted secret is a secret when using secret auth or the private key belonging to a
// client certificate used in client cert authentication.
secret, err = cryptoCodec.Decrypt(key, notifier.GetNotifierSecret())
if err != nil {
return nil, errors.Errorf("Error decrypting notifier secret for notifier %q", notifier.GetName())
}
}

// Tries to build authentication token for Client Cert Auth, if this is not possible proceed to create token credentials
// for secrets.
var azureTokenCredential azcore.TokenCredential
var authErrList = errorhelpers.NewErrorList("Sentinel authentication")
if config.GetClientCertAuthConfig().GetClientCert() != "" && config.GetClientCertAuthConfig().GetPrivateKey() != "" {
if config.GetClientCertAuthConfig().GetClientCert() != "" {
certs, err := x509utils.ConvertPEMTox509Certs([]byte(config.GetClientCertAuthConfig().GetClientCert()))
if err != nil {
return nil, errors.Wrap(err, "invalid cert")
}

keyBlock, _ := pem.Decode([]byte(config.GetClientCertAuthConfig().GetPrivateKey()))
keyBlock, rest := pem.Decode([]byte(secret))
if len(rest) != 0 {
log.Errorf("PEM was not valid, could not parse all data: %s", string(rest))
return nil, errors.Errorf("PEM was not valid, could not parse all data: %s", string(rest))
}
if keyBlock == nil {
return nil, errors.New("Could not parse empty key")
}
privateKey, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
if err != nil {
return nil, errors.Wrap(err, "could not parse azure sentinel private key")
Expand All @@ -99,9 +133,10 @@ func newSentinelNotifier(notifier *storage.Notifier) (*sentinel, error) {
}
}

if config.GetSecret() != "" && azureTokenCredential == nil {
// If client cert authentication is not configured use secret auth.
if config.GetClientCertAuthConfig().GetClientCert() == "" && azureTokenCredential == nil {
var err error
azureTokenCredential, err = azidentity.NewClientSecretCredential(config.GetDirectoryTenantId(), config.GetApplicationClientId(), config.GetSecret(), &azidentity.ClientSecretCredentialOptions{})
azureTokenCredential, err = azidentity.NewClientSecretCredential(config.GetDirectoryTenantId(), config.GetApplicationClientId(), secret, &azidentity.ClientSecretCredentialOptions{})
if err != nil {
authErrList.AddError(errors.Wrap(err, "could not create azure credentials with secret"))
}
Expand Down
31 changes: 30 additions & 1 deletion central/notifiers/microsoftsentinel/sentinel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
_ "embed"
"encoding/base64"
"io"
"net/http"
"testing"
Expand All @@ -12,7 +13,10 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/monitor/ingestion/azlogs"
"github.com/stackrox/rox/central/notifiers/microsoftsentinel/mocks"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/cryptoutils/cryptocodec"
"github.com/stackrox/rox/pkg/env"
"github.com/stackrox/rox/pkg/features"
pkgNotifiers "github.com/stackrox/rox/pkg/notifiers"
"github.com/stackrox/rox/pkg/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -296,15 +300,40 @@ func (suite *SentinelTestSuite) TestNewSentinelNotifier() {
PrivateKey: sentinelCaKey,
}

notifier, err := newSentinelNotifier(config)
notifier, err := newSentinelNotifier(config, nil, "")

suite.Require().NoError(err)
suite.NotNil(notifier)
}

func (suite *SentinelTestSuite) TestEncryption() {
suite.T().Setenv(env.EncNotifierCreds.EnvVar(), "true")

var exampleKey = []byte("key-string-12345")
b64EncodedKey := base64.StdEncoding.EncodeToString(exampleKey)
encryptedSecret, err := cryptocodec.NewGCMCryptoCodec().Encrypt(b64EncodedKey, "secret-for-sentinel")
suite.Require().NoError(err)

config := getNotifierConfig()
config.NotifierSecret = encryptedSecret

sentinelNotifier, err := newSentinelNotifier(config, cryptocodec.Singleton(), b64EncodedKey)

suite.Require().NoError(err)
suite.Require().NotNil(sentinelNotifier)

// test with invalid secret encryption should fail
config.NotifierSecret = ""

sentinelNotifier, err = newSentinelNotifier(config, cryptocodec.Singleton(), b64EncodedKey)
suite.ErrorContains(err, "Error decrypting notifier secret for notifier \"microsoft-sentinel\"")
suite.Nil(sentinelNotifier)
}

func getNotifierConfig() *storage.Notifier {
return &storage.Notifier{
Name: "microsoft-sentinel",
Type: pkgNotifiers.MicrosoftSentinelType,
Config: &storage.Notifier_MicrosoftSentinel{
MicrosoftSentinel: &storage.MicrosoftSentinel{
LogIngestionEndpoint: "portal.azure.com",
Expand Down
20 changes: 17 additions & 3 deletions central/notifiers/utils/encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func SecureNotifier(notifier *storage.Notifier, key string) error {
if secured {
return nil
}
creds, err := getCredentials(notifier)
creds, err := GetCredentials(notifier)
if err != nil {
return err
}
Expand All @@ -109,7 +109,7 @@ func IsNotifierSecured(notifier *storage.Notifier) (bool, error) {
// So just checking if the field NotifierSecret is non-empty is enough.
return notifier.GetNotifierSecret() != "", nil
}
creds, err := getCredentials(notifier)
creds, err := GetCredentials(notifier)
if err != nil {
return false, nil
}
Expand Down Expand Up @@ -141,7 +141,7 @@ func RekeyNotifier(notifier *storage.Notifier, oldKey string, newKey string) err
return err
}

func getCredentials(notifier *storage.Notifier) (string, error) {
func GetCredentials(notifier *storage.Notifier) (string, error) {
if notifier.GetConfig() == nil {
return "", nil
}
Expand All @@ -158,6 +158,11 @@ func getCredentials(notifier *storage.Notifier) (string, error) {
return notifier.GetPagerduty().GetApiKey(), nil
case pkgNotifiers.GenericType:
return notifier.GetGeneric().GetPassword(), nil
case pkgNotifiers.MicrosoftSentinelType:
if notifier.GetMicrosoftSentinel().GetSecret() != "" {
return notifier.GetMicrosoftSentinel().GetSecret(), nil
}
return notifier.GetMicrosoftSentinel().GetClientCertAuthConfig().GetPrivateKey(), nil
case pkgNotifiers.AWSSecurityHubType:
creds := notifier.GetAwsSecurityHub().GetCredentials()
if creds != nil {
Expand All @@ -168,6 +173,7 @@ func getCredentials(notifier *storage.Notifier) (string, error) {
return string(marshalled), nil
}
}

return "", nil
}

Expand Down Expand Up @@ -214,5 +220,13 @@ func cleanupCredentials(notifier *storage.Notifier) {
creds.AccessKeyId = ""
creds.SecretAccessKey = ""
}
case pkgNotifiers.MicrosoftSentinelType:
sentinel := notifier.GetMicrosoftSentinel()
if sentinel != nil {
if sentinel.GetClientCertAuthConfig() != nil {
sentinel.GetClientCertAuthConfig().PrivateKey = ""
}
sentinel.Secret = ""
}
}
}
65 changes: 65 additions & 0 deletions central/notifiers/utils/encryption_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,41 @@ func (s *NotifierSecurityTestSuite) TestSecureCleanupDisabled() {
s.Require().NotEmpty(awsSecurityHub.GetAwsSecurityHub().GetCredentials().GetAccessKeyId())
s.Require().NotEmpty(awsSecurityHub.GetAwsSecurityHub().GetCredentials().GetSecretAccessKey())
s.checkSecured(awsSecurityHub)

// Case: secure microsoft sentinel notifier
microsoftSentinel := &storage.Notifier{
Type: pkgNotifiers.MicrosoftSentinelType,
Config: &storage.Notifier_MicrosoftSentinel{
MicrosoftSentinel: &storage.MicrosoftSentinel{
Secret: "secret value",
},
},
}
s.checkUnsecured(microsoftSentinel)
err = SecureNotifier(microsoftSentinel, s.key)
s.Require().NoError(err)
s.Require().NotEmpty(microsoftSentinel.GetNotifierSecret())
s.Require().NotEmpty(microsoftSentinel.GetMicrosoftSentinel().GetSecret())
s.checkSecured(microsoftSentinel)

microsoftSentinel = &storage.Notifier{
Type: pkgNotifiers.MicrosoftSentinelType,
Config: &storage.Notifier_MicrosoftSentinel{
MicrosoftSentinel: &storage.MicrosoftSentinel{
ClientCertAuthConfig: &storage.MicrosoftSentinel_ClientCertAuthConfig{
PrivateKey: "private key",
ClientCert: "client cert",
},
},
},
}
s.checkUnsecured(microsoftSentinel)
err = SecureNotifier(microsoftSentinel, s.key)
s.Require().NoError(err)
s.Require().NotEmpty(microsoftSentinel.GetNotifierSecret())
s.Require().NotEmpty(microsoftSentinel.GetMicrosoftSentinel().GetClientCertAuthConfig().GetPrivateKey())
s.Require().NotEmpty(microsoftSentinel.GetMicrosoftSentinel().GetClientCertAuthConfig().GetClientCert())
s.checkSecured(microsoftSentinel)
}

func (s *NotifierSecurityTestSuite) TestSecureCleanupEnabled() {
Expand Down Expand Up @@ -433,6 +468,36 @@ func (s *NotifierSecurityTestSuite) TestSecureCleanupEnabled() {
s.Require().Empty(awsSecurityHub.GetAwsSecurityHub().GetCredentials().GetAccessKeyId())
s.Require().Empty(awsSecurityHub.GetAwsSecurityHub().GetCredentials().GetSecretAccessKey())
s.checkSecured(awsSecurityHub)

// Case: secure microsoft sentinel notifier
microsoftSentinel := &storage.Notifier{
Type: pkgNotifiers.MicrosoftSentinelType,
Config: &storage.Notifier_MicrosoftSentinel{
MicrosoftSentinel: &storage.MicrosoftSentinel{
Secret: "secret value",
},
},
}
s.checkUnsecured(microsoftSentinel)
err = SecureNotifier(microsoftSentinel, s.key)
s.Require().NoError(err)
s.Require().NotEmpty(microsoftSentinel.GetNotifierSecret())
s.Require().Empty(microsoftSentinel.GetMicrosoftSentinel().GetSecret())
s.Require().Empty(microsoftSentinel.GetMicrosoftSentinel().GetClientCertAuthConfig().GetPrivateKey())
s.checkSecured(microsoftSentinel)

microsoftSentinel.GetMicrosoftSentinel().ClientCertAuthConfig = &storage.MicrosoftSentinel_ClientCertAuthConfig{
PrivateKey: "private key",
ClientCert: "client cert",
}
s.checkUnsecured(microsoftSentinel)
err = SecureNotifier(microsoftSentinel, s.key)
s.Require().NoError(err)
s.Require().NotEmpty(microsoftSentinel.GetNotifierSecret())
s.Require().Empty(microsoftSentinel.GetMicrosoftSentinel().GetSecret())
s.Require().Empty(microsoftSentinel.GetMicrosoftSentinel().GetClientCertAuthConfig().GetPrivateKey())
s.Require().NotEmpty(microsoftSentinel.GetMicrosoftSentinel().GetClientCertAuthConfig().GetClientCert())
s.checkSecured(microsoftSentinel)
}

func (s *NotifierSecurityTestSuite) checkSecured(notifier *storage.Notifier) {
Expand Down
26 changes: 26 additions & 0 deletions dev-tools/create-notifier-secret.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -eo pipefail
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

cd ..

key_string=$(echo "key-string-1234" | base64)
echo "
apiVersion: v1
stringData:
key-chain.yaml: |
keyMap:
0: $key_string
activeKeyId: 0
kind: Secret
metadata:
name: central-encryption-key-chain
namespace: stackrox
type: Opaque
" | kubectl -n stackrox apply -f -

kubectl -n stackrox set env deployment/central ROX_ENC_NOTIFIER_CREDS=true

make -C "$DIR/../" cli-linux

"$DIR"/debug-helm-chart.sh -n stackrox upgrade stackrox-central-services --set central.notifierSecretsEncryption.enabled=true --reuse-values "$DIR"/../stackrox-central-services-chart
7 changes: 5 additions & 2 deletions dev-tools/debug-helm-chart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ set -eo pipefail

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

"$DIR/roxctl.sh" helm output central-services --image-defaults=development_build --remove --debug
"$DIR/roxctl.sh" helm output secured-cluster-services --image-defaults=development_build --remove --debug
# Set $IMAGE_DEFAULTS to change the image defaults. Use opensource to use the stackrox-io images.
image_defaults=${IMAGE_DEFAULTS:-"development_build"}

"$DIR/roxctl.sh" helm output central-services --image-defaults="$image_defaults" --remove --debug
"$DIR/roxctl.sh" helm output secured-cluster-services --image-defaults="$image_defaults" --remove --debug

helm "$@"
3 changes: 1 addition & 2 deletions generated/api/v1/notifier_service.swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading