Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
eaa0906
Create certSecretsRepo type to store certificates in k8s secrets
Jan 26, 2022
6cb9a1c
fix style
Jan 26, 2022
86c98e1
fix flaky test
Jan 27, 2022
d756b9e
add comments to clarify responsibilities of this code
Jan 31, 2022
a033701
remove retries, leave it to caller
Jan 31, 2022
16a5e18
use longer test timeout for increased stability on CI
Jan 31, 2022
e6e7086
only accept a single secret name in `newFixture`
Jan 31, 2022
7811e3a
use "stackrox-ns" as test namespace
Jan 31, 2022
6434c94
code cleanup
Feb 1, 2022
42d64a5
improve comments
Feb 1, 2022
2fc1013
simplify tests delegating test timeout to CI task
Feb 1, 2022
91f3db9
put fails on unknown secrets and it's ok on missing secrets
Feb 1, 2022
4786c64
comment that put is idempotent but not atomic
Feb 2, 2022
2f5772c
wip convert secrets repo into certificates repo
Feb 2, 2022
8f84176
add unit tests
Feb 2, 2022
f411d16
rename repository files accordingly to refactor
Feb 2, 2022
6de90b6
improve comments and additional input validation
Feb 2, 2022
f1b6e8c
check same CA is retrieved on get
Feb 3, 2022
9b6a6dc
simplify factory function for repo
Feb 3, 2022
7bb589e
add parameters for secrets file paths
Feb 3, 2022
d0747af
move body of put and get certificates loop to separte methods
Feb 3, 2022
2854724
rename `f` to `fixture` to improve readability
Feb 3, 2022
ccdfe68
allow different certificate file path per service type
Feb 4, 2022
ec2c8f7
check secret ownership and create secrets on the repository
Feb 7, 2022
4f1bad1
comments improvements
Feb 7, 2022
a3111f9
request certificates on demand during secret setup
Feb 8, 2022
358f02d
move repo interface definition to client code
Feb 8, 2022
369837e
fix secret owner reference
Feb 9, 2022
744cdab
make GetServiceCertificates functional instead of mutating a param
Feb 9, 2022
9b0b29c
check secret ownership on get certificates
Feb 10, 2022
f8f1884
replace setupSecrets by CreateSecrets
Feb 10, 2022
ca39006
make methods private
Feb 10, 2022
963539c
simplify owner reference management
Feb 10, 2022
64ea16b
polish comments
Feb 10, 2022
3650011
always create the secrets in createSecrets
Feb 10, 2022
8456e48
return highest priority error in getServiceCertificates
Feb 10, 2022
b0f5e69
use our "errors" package
Feb 11, 2022
5fc7892
return multierror in get and put
Feb 11, 2022
fcb0a10
just check the UID of the owner reference in get
Feb 11, 2022
0fd69a3
use named parameters in test case structs
Feb 11, 2022
4c52fbb
use s.ErrorIs instead of ad-hoc function
Feb 11, 2022
a954e8f
improve names of test cases due to k8s API errors
Feb 11, 2022
e0eda1e
improve readability of newFixture
Feb 11, 2022
45b6a81
properly cancel on context cancellation
Feb 11, 2022
32e1cb4
unify put and update in ensure
Feb 11, 2022
931609c
rename certSecretsRepoFixtureSpec to certSecretsRepoFixtureConfig
Feb 14, 2022
35792e4
remove references to put
Feb 14, 2022
70b4f02
complete tests
Feb 14, 2022
0bc9854
fix comment
Feb 14, 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
237 changes: 237 additions & 0 deletions sensor/kubernetes/localscanner/service_certificates_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package localscanner

import (
"bytes"
"context"
"encoding/json"

"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/mtls"
v1 "k8s.io/api/core/v1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sTypes "k8s.io/apimachinery/pkg/types"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)

var (
// ErrUnexpectedSecretsOwner indicates that this repository should not be updating the certificates in
// the secrets they do not have the expected owner.
ErrUnexpectedSecretsOwner = errors.New("unexpected owner for certificate secrets")

// ErrDifferentCAForDifferentServiceTypes indicates that different service types have different values
// for CA stored in their secrets.
ErrDifferentCAForDifferentServiceTypes = errors.New("found different CA PEM in secret Data for different service types")

// ErrMissingSecretData indicates some secret has no data.
ErrMissingSecretData = errors.New("missing secret data")

errForServiceFormat = "for service type %q"
)

// serviceCertificatesRepoSecretsImpl is a ServiceCertificatesRepo that uses k8s secrets for persistence.
type serviceCertificatesRepoSecretsImpl struct {
secrets map[storage.ServiceType]serviceCertSecretSpec
ownerReference metav1.OwnerReference
namespace string
secretsClient corev1.SecretInterface
}

// serviceCertSecretSpec specifies the name of the secret where certificates for a service are stored, and
// the secret data keys where each certificate file is stored.
type serviceCertSecretSpec struct {
secretName string
caCertFileName string
serviceCertFileName string
serviceKeyFileName string
}

// newServiceCertificatesRepo creates a new serviceCertificatesRepoSecretsImpl that persists certificates for
// scanner and scanner DB in k8s secrets that are expected to have ownerReference as the only owner reference.
func newServiceCertificatesRepo(ownerReference metav1.OwnerReference, namespace string,
secretsClient corev1.SecretInterface) *serviceCertificatesRepoSecretsImpl {

return &serviceCertificatesRepoSecretsImpl{
secrets: map[storage.ServiceType]serviceCertSecretSpec{
storage.ServiceType_SCANNER_SERVICE: {
secretName: "scanner-slim-tls",
caCertFileName: mtls.CACertFileName,
serviceCertFileName: mtls.ServiceCertFileName,
serviceKeyFileName: mtls.ServiceKeyFileName,
},
storage.ServiceType_SCANNER_DB_SERVICE: {
secretName: "scanner-db-slim-tls",
caCertFileName: mtls.CACertFileName,
serviceCertFileName: mtls.ServiceCertFileName,
serviceKeyFileName: mtls.ServiceKeyFileName,
},
},
ownerReference: ownerReference,
namespace: namespace,
secretsClient: secretsClient,
}
}

// getServiceCertificates fails as soon as the context is cancelled. Otherwise it returns a multierror that can contain
// the following errors:
// - ErrUnexpectedSecretsOwner in case the owner specified in the constructor is not the sole owner of all secrets.
// - ErrMissingSecretData in case any secret has no data.
// - ErrDifferentCAForDifferentServiceTypes in case the CA is not the same in all secrets.
// If the data for a secret is missing some expecting key then the corresponding field in the TypedServiceCertificate.
// for that secret will contain a zero value.
func (r *serviceCertificatesRepoSecretsImpl) getServiceCertificates(ctx context.Context) (*storage.TypedServiceCertificateSet, error) {
certificates := &storage.TypedServiceCertificateSet{}
certificates.ServiceCerts = make([]*storage.TypedServiceCertificate, 0)
var getErr error
for serviceType, secretSpec := range r.secrets {
// on context cancellation abort getting other secrets.
if ctx.Err() != nil {
return nil, ctx.Err()
}

certificate, ca, err := r.getServiceCertificate(ctx, serviceType, secretSpec)
if err != nil {
getErr = multierror.Append(getErr, err)
continue
}
if certificates.GetCaPem() == nil {
certificates.CaPem = ca
} else {
if !bytes.Equal(certificates.GetCaPem(), ca) {
getErr = multierror.Append(getErr, ErrDifferentCAForDifferentServiceTypes)
continue
}
}
certificates.ServiceCerts = append(certificates.ServiceCerts, certificate)
}

if getErr != nil {
return nil, getErr
}

return certificates, nil
}

func (r *serviceCertificatesRepoSecretsImpl) getServiceCertificate(ctx context.Context, serviceType storage.ServiceType,
secretSpec serviceCertSecretSpec) (cert *storage.TypedServiceCertificate, ca []byte, err error) {

secret, getErr := r.secretsClient.Get(ctx, secretSpec.secretName, metav1.GetOptions{})
if getErr != nil {
return nil, nil, getErr
}

ownerReferences := secret.GetOwnerReferences()
if len(ownerReferences) != 1 {
return nil, nil, ErrUnexpectedSecretsOwner
}

if ownerReferences[0].UID != r.ownerReference.UID {
return nil, nil, ErrUnexpectedSecretsOwner
}

secretData := secret.Data
if secretData == nil {
return nil, nil, ErrMissingSecretData
}

return &storage.TypedServiceCertificate{
ServiceType: serviceType,
Cert: &storage.ServiceCertificate{
CertPem: secretData[secretSpec.serviceCertFileName],
KeyPem: secretData[secretSpec.serviceKeyFileName],
},
}, secretData[secretSpec.caCertFileName], nil
}

// ensureServiceCertificates ensures the services for certificates exists, and that they contain the certificates
// in their data.
// This operation is idempotent but not atomic in sense that on error some secrets might be created and updated,
// while others are not.
// Each missing secret is created with the owner specified in the constructor as owner.
// This only creates secrets for the service types that appear in certificates, missing service types are just skipped.
// Fails for certificates with a service type that doesn't appear in r.secrets, as we don't know where to store them.
func (r *serviceCertificatesRepoSecretsImpl) ensureServiceCertificates(ctx context.Context,
certificates *storage.TypedServiceCertificateSet) error {

caPem := certificates.GetCaPem()
var serviceErrors error
for _, cert := range certificates.GetServiceCerts() {
// on context cancellation abort putting other secrets.
if ctx.Err() != nil {
return ctx.Err()
}

secretSpec, ok := r.secrets[cert.GetServiceType()]
if !ok {
// we don't know how to persist this.
err := errors.Errorf("unkown service type %q", cert.GetServiceType())
serviceErrors = multierror.Append(serviceErrors, err)
continue
}
if err := r.ensureServiceCertificate(ctx, caPem, cert, secretSpec); err != nil {
serviceErrors = multierror.Append(serviceErrors, err)
}
}

return serviceErrors
}

func (r *serviceCertificatesRepoSecretsImpl) ensureServiceCertificate(ctx context.Context, caPem []byte,
cert *storage.TypedServiceCertificate, secretSpec serviceCertSecretSpec) error {
patchErr := r.patchServiceCertificate(ctx, caPem, cert, secretSpec)
if k8sErrors.IsNotFound(patchErr) {
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.

I like the simplicity and readability of this function!

_, createErr := r.createSecret(ctx, caPem, cert, secretSpec)
return createErr
}
return patchErr
}

func (r *serviceCertificatesRepoSecretsImpl) patchServiceCertificate(ctx context.Context, caPem []byte,
cert *storage.TypedServiceCertificate, secretSpec serviceCertSecretSpec) error {
patch := []patchSecretDataByteMap{{
Op: "replace",
Path: "/data",
Value: r.secretDataForCertificate(secretSpec, caPem, cert),
}}
patchBytes, marshallingErr := json.Marshal(patch)
if marshallingErr != nil {
return errors.Wrapf(marshallingErr, errForServiceFormat, cert.GetServiceType())
}
if _, patchErr := r.secretsClient.Patch(ctx, secretSpec.secretName, k8sTypes.JSONPatchType, patchBytes,
metav1.PatchOptions{}); patchErr != nil {
return errors.Wrapf(patchErr, errForServiceFormat, cert.GetServiceType())
}

return nil
}

type patchSecretDataByteMap struct {
Op string `json:"op"`
Path string `json:"path"`
Value map[string][]byte `json:"value"`
}

func (r *serviceCertificatesRepoSecretsImpl) createSecret(ctx context.Context, caPem []byte,
certificate *storage.TypedServiceCertificate, secretSpec serviceCertSecretSpec) (*v1.Secret, error) {

return r.secretsClient.Create(ctx, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretSpec.secretName,
Namespace: r.namespace,
OwnerReferences: []metav1.OwnerReference{r.ownerReference},
},
Data: r.secretDataForCertificate(secretSpec, caPem, certificate),
}, metav1.CreateOptions{})
}

func (r *serviceCertificatesRepoSecretsImpl) secretDataForCertificate(secretSpec serviceCertSecretSpec, caPem []byte,
cert *storage.TypedServiceCertificate) map[string][]byte {

return map[string][]byte{
secretSpec.caCertFileName: caPem,
secretSpec.serviceCertFileName: cert.GetCert().GetCertPem(),
secretSpec.serviceKeyFileName: cert.GetCert().GetKeyPem(),
}
}
Loading