-
Notifications
You must be signed in to change notification settings - Fork 174
ROX-9128: Create certSecretsRepo type to store certificates in k8s secrets #457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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
6cb9a1c
fix style
86c98e1
fix flaky test
d756b9e
add comments to clarify responsibilities of this code
a033701
remove retries, leave it to caller
16a5e18
use longer test timeout for increased stability on CI
e6e7086
only accept a single secret name in `newFixture`
7811e3a
use "stackrox-ns" as test namespace
6434c94
code cleanup
42d64a5
improve comments
2fc1013
simplify tests delegating test timeout to CI task
91f3db9
put fails on unknown secrets and it's ok on missing secrets
4786c64
comment that put is idempotent but not atomic
2f5772c
wip convert secrets repo into certificates repo
8f84176
add unit tests
f411d16
rename repository files accordingly to refactor
6de90b6
improve comments and additional input validation
f1b6e8c
check same CA is retrieved on get
9b6a6dc
simplify factory function for repo
7bb589e
add parameters for secrets file paths
d0747af
move body of put and get certificates loop to separte methods
2854724
rename `f` to `fixture` to improve readability
ccdfe68
allow different certificate file path per service type
ec2c8f7
check secret ownership and create secrets on the repository
4f1bad1
comments improvements
a3111f9
request certificates on demand during secret setup
358f02d
move repo interface definition to client code
369837e
fix secret owner reference
744cdab
make GetServiceCertificates functional instead of mutating a param
9b0b29c
check secret ownership on get certificates
f8f1884
replace setupSecrets by CreateSecrets
ca39006
make methods private
963539c
simplify owner reference management
64ea16b
polish comments
3650011
always create the secrets in createSecrets
8456e48
return highest priority error in getServiceCertificates
b0f5e69
use our "errors" package
5fc7892
return multierror in get and put
fcb0a10
just check the UID of the owner reference in get
0fd69a3
use named parameters in test case structs
4c52fbb
use s.ErrorIs instead of ad-hoc function
a954e8f
improve names of test cases due to k8s API errors
e0eda1e
improve readability of newFixture
45b6a81
properly cancel on context cancellation
32e1cb4
unify put and update in ensure
931609c
rename certSecretsRepoFixtureSpec to certSecretsRepoFixtureConfig
35792e4
remove references to put
70b4f02
complete tests
0bc9854
fix comment
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
237 changes: 237 additions & 0 deletions
237
sensor/kubernetes/localscanner/service_certificates_repository.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
| _, 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, | ||
SimonBaeumer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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(), | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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!