diff --git a/.gitleaksignore b/.gitleaksignore index 843314248..b28448e85 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1,2 +1,3 @@ 791662e0244f1e326975d9010a611a55d4506623:internal/tls/validate_test.go:private-key:10 -304d906b7d106982bf2fe8fe935a64da3b0fe22d:internal/tls/validate_test.go:private-key:23 \ No newline at end of file +304d906b7d106982bf2fe8fe935a64da3b0fe22d:internal/tls/validate_test.go:private-key:23 +f7d7d265cb40c46f876bc0a825d7efffbbcc05db:internal/tls/cert/tls_cert_validator_test.go:private-key:26 \ No newline at end of file diff --git a/docs/user/resources/02-logpipeline.md b/docs/user/resources/02-logpipeline.md index 40d39767e..d1c11301e 100644 --- a/docs/user/resources/02-logpipeline.md +++ b/docs/user/resources/02-logpipeline.md @@ -199,10 +199,17 @@ The status of the LogPipeline is determined by the condition types `AgentHealthy > **NOTE:** The condition types `Running` and `Pending` are deprecated and will be removed soon from the status conditions. -| Condition Type | Condition Status | Condition Reason | Condition Message | -|------------------------|------------------|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| AgentHealthy | True | DaemonSetReady | Fluent Bit DaemonSet is ready | -| AgentHealthy | False | DaemonSetNotReady | Fluent Bit DaemonSet is not ready | -| ConfigurationGenerated | True | ConfigurationGenerated | | -| ConfigurationGenerated | False | ReferencedSecretMissing | One or more referenced Secrets are missing | +| Condition Type | Condition Status | Condition Reason | Condition Message | +|------------------------|------------------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| AgentHealthy | True | DaemonSetReady | Fluent Bit DaemonSet is ready | +| AgentHealthy | False | DaemonSetNotReady | Fluent Bit DaemonSet is not ready | +| ConfigurationGenerated | True | ConfigurationGenerated | | +| ConfigurationGenerated | False | ReferencedSecretMissing | One or more referenced Secrets are missing | | ConfigurationGenerated | False | UnsupportedLokiOutput | grafana-loki output is not supported anymore. For integration with a custom Loki installation, use the `custom` output and follow [Intergrate with Loki](https://kyma-project.io/#/telemetry-manager/user/integration/loki/README). | +| ConfigurationGenerated | False | InvalidTLSCert | TLS certificate invalid | +| ConfigurationGenerated | False | InvalidTLSPrivateKey | TLS private key invalid | +| ConfigurationGenerated | False | ExpiredTLSCert | TLS certificate expired on YYYY-MM-DD | +| ConfigurationGenerated | True | TLSCertAboutToExpire | TLS certificate is about to expire, configured certificate is valid until YYYY-MM-DD | + + + diff --git a/internal/conditions/conditions.go b/internal/conditions/conditions.go index 87a0da8d9..fe254c13d 100644 --- a/internal/conditions/conditions.go +++ b/internal/conditions/conditions.go @@ -18,20 +18,24 @@ const ( ) const ( - ReasonNoPipelineDeployed = "NoPipelineDeployed" - ReasonReferencedSecretMissing = "ReferencedSecretMissing" - ReasonMaxPipelinesExceeded = "MaxPipelinesExceeded" - ReasonResourceBlocksDeletion = "ResourceBlocksDeletion" - ReasonConfigurationGenerated = "ConfigurationGenerated" - ReasonDeploymentNotReady = "DeploymentNotReady" - ReasonDeploymentReady = "DeploymentReady" - ReasonDaemonSetNotReady = "DaemonSetNotReady" - ReasonDaemonSetReady = "DaemonSetReady" - ReasonAllDataDropped = "AllTelemetryDataDropped" - ReasonSomeDataDropped = "SomeTelemetryDataDropped" - ReasonBufferFillingUp = "BufferFillingUp" - ReasonGatewayThrottling = "GatewayThrottling" - ReasonFlowHealthy = "Healthy" + ReasonNoPipelineDeployed = "NoPipelineDeployed" + ReasonReferencedSecretMissing = "ReferencedSecretMissing" + ReasonMaxPipelinesExceeded = "MaxPipelinesExceeded" + ReasonResourceBlocksDeletion = "ResourceBlocksDeletion" + ReasonConfigurationGenerated = "ConfigurationGenerated" + ReasonDeploymentNotReady = "DeploymentNotReady" + ReasonDeploymentReady = "DeploymentReady" + ReasonDaemonSetNotReady = "DaemonSetNotReady" + ReasonDaemonSetReady = "DaemonSetReady" + ReasonAllDataDropped = "AllTelemetryDataDropped" + ReasonSomeDataDropped = "SomeTelemetryDataDropped" + ReasonBufferFillingUp = "BufferFillingUp" + ReasonGatewayThrottling = "GatewayThrottling" + ReasonFlowHealthy = "Healthy" + ReasonTLSCertificateInvalid = "TLSCertificateInvalid" + ReasonTLSPrivateKeyInvalid = "TLSPrivateKeyInvalid" + ReasonTLSCertificateExpired = "TLSCertificateExpired" + ReasonTLSCertificateAboutToExpire = "TLSCertAboutToExpire" ReasonMetricAgentNotRequired = "AgentNotRequired" ReasonMetricComponentsRunning = "MetricComponentsRunning" @@ -83,12 +87,16 @@ var tracePipelineMessages = map[string]string{ } var logPipelineMessages = map[string]string{ - ReasonDaemonSetNotReady: "Fluent Bit DaemonSet is not ready", - ReasonDaemonSetReady: "Fluent Bit DaemonSet is ready", - ReasonFluentBitDSNotReady: "Fluent Bit DaemonSet is not ready", - ReasonFluentBitDSReady: "Fluent Bit DaemonSet is ready", - ReasonUnsupportedLokiOutput: "grafana-loki output is not supported anymore. For integration with a custom Loki installation, use the `custom` output and follow https://kyma-project.io/#/telemetry-manager/user/integration/loki/README", - ReasonLogComponentsRunning: "All log components are running", + ReasonDaemonSetNotReady: "Fluent Bit DaemonSet is not ready", + ReasonDaemonSetReady: "Fluent Bit DaemonSet is ready", + ReasonFluentBitDSNotReady: "Fluent Bit DaemonSet is not ready", + ReasonFluentBitDSReady: "Fluent Bit DaemonSet is ready", + ReasonUnsupportedLokiOutput: "grafana-loki output is not supported anymore. For integration with a custom Loki installation, use the `custom` output and follow https://kyma-project.io/#/telemetry-manager/user/integration/loki/README", + ReasonLogComponentsRunning: "All log components are running", + ReasonTLSCertificateInvalid: "TLS certificate invalid: %s", + ReasonTLSPrivateKeyInvalid: "TLS private key invalid: %s", + ReasonTLSCertificateExpired: "TLS certificate expired on %s", + ReasonTLSCertificateAboutToExpire: "TLS certificate is about to expire, configured certificate is valid until %s", } func MessageForLogPipeline(reason string) string { diff --git a/internal/reconciler/logpipeline/mocks/daemon_set_annotator.go b/internal/reconciler/logpipeline/mocks/daemon_set_annotator.go index 77a4379f1..6b67ae5e8 100644 --- a/internal/reconciler/logpipeline/mocks/daemon_set_annotator.go +++ b/internal/reconciler/logpipeline/mocks/daemon_set_annotator.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package mocks @@ -19,6 +19,10 @@ type DaemonSetAnnotator struct { func (_m *DaemonSetAnnotator) SetAnnotation(ctx context.Context, name types.NamespacedName, key string, value string) error { ret := _m.Called(ctx, name, key, value) + if len(ret) == 0 { + panic("no return value specified for SetAnnotation") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, types.NamespacedName, string, string) error); ok { r0 = rf(ctx, name, key, value) @@ -29,13 +33,12 @@ func (_m *DaemonSetAnnotator) SetAnnotation(ctx context.Context, name types.Name return r0 } -type mockConstructorTestingTNewDaemonSetAnnotator interface { +// NewDaemonSetAnnotator creates a new instance of DaemonSetAnnotator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDaemonSetAnnotator(t interface { mock.TestingT Cleanup(func()) -} - -// NewDaemonSetAnnotator creates a new instance of DaemonSetAnnotator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewDaemonSetAnnotator(t mockConstructorTestingTNewDaemonSetAnnotator) *DaemonSetAnnotator { +}) *DaemonSetAnnotator { mock := &DaemonSetAnnotator{} mock.Mock.Test(t) diff --git a/internal/reconciler/logpipeline/mocks/daemon_set_prober.go b/internal/reconciler/logpipeline/mocks/daemon_set_prober.go index 1e5609c6b..8e11d2254 100644 --- a/internal/reconciler/logpipeline/mocks/daemon_set_prober.go +++ b/internal/reconciler/logpipeline/mocks/daemon_set_prober.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package mocks @@ -19,14 +19,21 @@ type DaemonSetProber struct { func (_m *DaemonSetProber) IsReady(ctx context.Context, name types.NamespacedName) (bool, error) { ret := _m.Called(ctx, name) + if len(ret) == 0 { + panic("no return value specified for IsReady") + } + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, types.NamespacedName) (bool, error)); ok { + return rf(ctx, name) + } if rf, ok := ret.Get(0).(func(context.Context, types.NamespacedName) bool); ok { r0 = rf(ctx, name) } else { r0 = ret.Get(0).(bool) } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, types.NamespacedName) error); ok { r1 = rf(ctx, name) } else { @@ -36,13 +43,12 @@ func (_m *DaemonSetProber) IsReady(ctx context.Context, name types.NamespacedNam return r0, r1 } -type mockConstructorTestingTNewDaemonSetProber interface { +// NewDaemonSetProber creates a new instance of DaemonSetProber. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDaemonSetProber(t interface { mock.TestingT Cleanup(func()) -} - -// NewDaemonSetProber creates a new instance of DaemonSetProber. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewDaemonSetProber(t mockConstructorTestingTNewDaemonSetProber) *DaemonSetProber { +}) *DaemonSetProber { mock := &DaemonSetProber{} mock.Mock.Test(t) diff --git a/internal/reconciler/logpipeline/mocks/tls_cert_validator.go b/internal/reconciler/logpipeline/mocks/tls_cert_validator.go new file mode 100644 index 000000000..96b6aaaa3 --- /dev/null +++ b/internal/reconciler/logpipeline/mocks/tls_cert_validator.go @@ -0,0 +1,46 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + cert "github.com/kyma-project/telemetry-manager/internal/tls/cert" + + mock "github.com/stretchr/testify/mock" +) + +// TLSCertValidator is an autogenerated mock type for the TLSCertValidator type +type TLSCertValidator struct { + mock.Mock +} + +// ValidateCertificate provides a mock function with given fields: certPEM, keyPEM +func (_m *TLSCertValidator) ValidateCertificate(certPEM []byte, keyPEM []byte) cert.TLSCertValidationResult { + ret := _m.Called(certPEM, keyPEM) + + if len(ret) == 0 { + panic("no return value specified for ValidateCertificate") + } + + var r0 cert.TLSCertValidationResult + if rf, ok := ret.Get(0).(func([]byte, []byte) cert.TLSCertValidationResult); ok { + r0 = rf(certPEM, keyPEM) + } else { + r0 = ret.Get(0).(cert.TLSCertValidationResult) + } + + return r0 +} + +// NewTLSCertValidator creates a new instance of TLSCertValidator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTLSCertValidator(t interface { + mock.TestingT + Cleanup(func()) +}) *TLSCertValidator { + mock := &TLSCertValidator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/reconciler/logpipeline/reconciler.go b/internal/reconciler/logpipeline/reconciler.go index d9f117f31..cd6509a9e 100644 --- a/internal/reconciler/logpipeline/reconciler.go +++ b/internal/reconciler/logpipeline/reconciler.go @@ -19,6 +19,7 @@ package logpipeline import ( "context" "fmt" + "time" "github.com/prometheus/client_golang/prometheus" corev1 "k8s.io/api/core/v1" @@ -38,6 +39,7 @@ import ( commonresources "github.com/kyma-project/telemetry-manager/internal/resources/common" "github.com/kyma-project/telemetry-manager/internal/resources/fluentbit" "github.com/kyma-project/telemetry-manager/internal/secretref" + "github.com/kyma-project/telemetry-manager/internal/tls/cert" ) type Config struct { @@ -64,6 +66,11 @@ type DaemonSetAnnotator interface { SetAnnotation(ctx context.Context, name types.NamespacedName, key, value string) error } +//go:generate mockery --name TLSCertValidator --filename tls_cert_validator.go +type TLSCertValidator interface { + ValidateCertificate(certPEM []byte, keyPEM []byte) cert.TLSCertValidationResult +} + type Reconciler struct { client.Client config Config @@ -73,6 +80,7 @@ type Reconciler struct { syncer syncer overridesHandler *overrides.Handler istioStatusChecker istiostatus.Checker + tlsCertValidator TLSCertValidator } func NewReconciler(client client.Client, config Config, prober DaemonSetProber, overridesHandler *overrides.Handler) *Reconciler { @@ -86,6 +94,7 @@ func NewReconciler(client client.Client, config Config, prober DaemonSetProber, r.syncer = syncer{client, config} r.overridesHandler = overridesHandler r.istioStatusChecker = istiostatus.NewChecker(client) + r.tlsCertValidator = &cert.TLSCertValidator{} return &r } @@ -136,7 +145,7 @@ func (r *Reconciler) doReconcile(ctx context.Context, pipeline *telemetryv1alpha return err } - deployableLogPipelines := getDeployableLogPipelines(ctx, allPipelines.Items, r.Client) + deployableLogPipelines := getDeployableLogPipelines(ctx, allPipelines.Items, r.Client, r.tlsCertValidator) if err = r.syncer.syncFluentBitConfig(ctx, pipeline, deployableLogPipelines); err != nil { return err } @@ -295,9 +304,11 @@ func (r *Reconciler) calculateChecksum(ctx context.Context) (string, error) { // getDeployableLogPipelines returns the list of log pipelines that are ready to be rendered into the Fluent Bit configuration. // A pipeline is deployable if it is not being deleted, all secret references exist, and it doesn't have the legacy grafana-loki output defined. -func getDeployableLogPipelines(ctx context.Context, allPipelines []telemetryv1alpha1.LogPipeline, client client.Client) []telemetryv1alpha1.LogPipeline { +func getDeployableLogPipelines(ctx context.Context, allPipelines []telemetryv1alpha1.LogPipeline, client client.Client, certValidator TLSCertValidator) []telemetryv1alpha1.LogPipeline { var deployablePipelines []telemetryv1alpha1.LogPipeline for i := range allPipelines { + certValidationResult := getTLSCertValidationResult(ctx, &allPipelines[i], certValidator, client) + if !allPipelines[i].GetDeletionTimestamp().IsZero() { continue } @@ -307,6 +318,10 @@ func getDeployableLogPipelines(ctx context.Context, allPipelines []telemetryv1al if allPipelines[i].Spec.Output.IsLokiDefined() { continue } + + if !certValidationResult.CertValid || !certValidationResult.PrivateKeyValid || time.Now().After(certValidationResult.Validity) { + continue + } deployablePipelines = append(deployablePipelines, allPipelines[i]) } @@ -319,3 +334,46 @@ func getFluentBitPorts() []int32 { ports.HTTP, } } + +func getTLSCertValidationResult(ctx context.Context, pipeline *telemetryv1alpha1.LogPipeline, validator TLSCertValidator, client client.Client) cert.TLSCertValidationResult { + if pipeline.Spec.Output.HTTP == nil || (pipeline.Spec.Output.HTTP.TLSConfig.Cert == nil && pipeline.Spec.Output.HTTP.TLSConfig.Key == nil) { + return cert.TLSCertValidationResult{ + CertValid: true, + PrivateKeyValid: true, + Validity: time.Now().AddDate(1, 0, 0), + } + } + + certValue := pipeline.Spec.Output.HTTP.TLSConfig.Cert + keyValue := pipeline.Spec.Output.HTTP.TLSConfig.Key + + certData, err := resolveValue(ctx, client, *certValue) + + if err != nil { + return cert.TLSCertValidationResult{ + CertValid: false, + } + } + + keyData, err := resolveValue(ctx, client, *keyValue) + + if err != nil { + return cert.TLSCertValidationResult{ + PrivateKeyValid: false, + } + } + + return validator.ValidateCertificate(certData, keyData) + +} + +func resolveValue(ctx context.Context, c client.Reader, value telemetryv1alpha1.ValueType) ([]byte, error) { + if value.Value != "" { + return []byte(value.Value), nil + } + if value.ValueFrom.IsSecretKeyRef() { + return secretref.GetValue(ctx, c, *value.ValueFrom.SecretKeyRef) + } + + return nil, fmt.Errorf("either value or secret key reference must be defined") +} diff --git a/internal/reconciler/logpipeline/reconciler_test.go b/internal/reconciler/logpipeline/reconciler_test.go index 439cc4544..300850391 100644 --- a/internal/reconciler/logpipeline/reconciler_test.go +++ b/internal/reconciler/logpipeline/reconciler_test.go @@ -3,6 +3,7 @@ package logpipeline import ( "context" "testing" + "time" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -13,6 +14,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" + "github.com/kyma-project/telemetry-manager/internal/reconciler/logpipeline/mocks" + "github.com/kyma-project/telemetry-manager/internal/tls/cert" ) func TestGetDeployableLogPipelines(t *testing.T) { @@ -106,6 +109,118 @@ func TestGetDeployableLogPipelines(t *testing.T) { }, deployablePipelines: true, }, + { + name: "should reject LogPipelines with invalid certificate", + pipelines: []telemetryv1alpha1.LogPipeline{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-with-invalid-cert", + }, + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.Output{ + HTTP: &telemetryv1alpha1.HTTPOutput{ + Host: telemetryv1alpha1.ValueType{ + Value: "http://somehost", + }, + TLSConfig: telemetryv1alpha1.TLSConfig{ + Key: &telemetryv1alpha1.ValueType{ + Value: "somekey", + }, + Cert: &telemetryv1alpha1.ValueType{ + Value: "invalidcert", + }, + }, + }, + }, + }, + }, + }, + deployablePipelines: false, + }, + { + name: "should reject LogPipelines with invalid certificate key", + pipelines: []telemetryv1alpha1.LogPipeline{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-with-invalid-cert-key", + }, + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.Output{ + HTTP: &telemetryv1alpha1.HTTPOutput{ + Host: telemetryv1alpha1.ValueType{ + Value: "http://somehost", + }, + TLSConfig: telemetryv1alpha1.TLSConfig{ + Key: &telemetryv1alpha1.ValueType{ + Value: "invalidkey", + }, + Cert: &telemetryv1alpha1.ValueType{ + Value: "somecert", + }, + }, + }, + }, + }, + }, + }, + deployablePipelines: false, + }, + { + name: "should reject LogPipelines with expired certificate", + pipelines: []telemetryv1alpha1.LogPipeline{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-with-expired-cert", + }, + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.Output{ + HTTP: &telemetryv1alpha1.HTTPOutput{ + Host: telemetryv1alpha1.ValueType{ + Value: "http://somehost", + }, + TLSConfig: telemetryv1alpha1.TLSConfig{ + Key: &telemetryv1alpha1.ValueType{ + Value: "expired", + }, + Cert: &telemetryv1alpha1.ValueType{ + Value: "expired", + }, + }, + }, + }, + }, + }, + }, + deployablePipelines: false, + }, + { + name: "should accept LogPipelines with valid certificate", + pipelines: []telemetryv1alpha1.LogPipeline{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-with-valid-cert", + }, + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.Output{ + HTTP: &telemetryv1alpha1.HTTPOutput{ + Host: telemetryv1alpha1.ValueType{ + Value: "http://somehost", + }, + TLSConfig: telemetryv1alpha1.TLSConfig{ + Key: &telemetryv1alpha1.ValueType{ + Value: "valid", + }, + Cert: &telemetryv1alpha1.ValueType{ + Value: "valid", + }, + }, + }, + }, + }, + }, + }, + deployablePipelines: true, + }, } for _, test := range tests { @@ -116,7 +231,26 @@ func TestGetDeployableLogPipelines(t *testing.T) { _ = telemetryv1alpha1.AddToScheme(scheme) fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() - deployablePipelines := getDeployableLogPipelines(ctx, test.pipelines, fakeClient) + validatorStub := &mocks.TLSCertValidator{} + + validatorStub.On("ValidateCertificate", []byte("invalidcert"), []byte("somekey")).Return(cert.TLSCertValidationResult{ + CertValid: false, + PrivateKeyValid: true, + Validity: time.Now().Add(time.Hour * 24 * 365), + }).On("ValidateCertificate", []byte("somecert"), []byte("invalidkey")).Return(cert.TLSCertValidationResult{ + CertValid: true, + PrivateKeyValid: false, + Validity: time.Now().Add(time.Hour * 24 * 365), + }).On("ValidateCertificate", []byte("valid"), []byte("valid")).Return(cert.TLSCertValidationResult{ + CertValid: true, + PrivateKeyValid: true, + Validity: time.Now().Add(time.Hour * 24 * 365), + }).On("ValidateCertificate", []byte("expired"), []byte("expired")).Return(cert.TLSCertValidationResult{ + CertValid: true, + PrivateKeyValid: true, + Validity: time.Now().AddDate(-1, -1, -1), + }) + deployablePipelines := getDeployableLogPipelines(ctx, test.pipelines, fakeClient, validatorStub) for _, pipeline := range test.pipelines { if test.deployablePipelines == true { require.Contains(t, deployablePipelines, pipeline) diff --git a/internal/reconciler/logpipeline/status.go b/internal/reconciler/logpipeline/status.go index 06bf8a351..746e01330 100644 --- a/internal/reconciler/logpipeline/status.go +++ b/internal/reconciler/logpipeline/status.go @@ -3,6 +3,7 @@ package logpipeline import ( "context" "fmt" + "time" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -15,6 +16,8 @@ import ( "github.com/kyma-project/telemetry-manager/internal/secretref" ) +const twoWeeks = time.Hour * 24 * 7 * 2 + func (r *Reconciler) updateStatus(ctx context.Context, pipelineName string) error { var pipeline telemetryv1alpha1.LogPipeline if err := r.Get(ctx, types.NamespacedName{Name: pipelineName}, &pipeline); err != nil { @@ -93,7 +96,7 @@ func (r *Reconciler) setAgentHealthyCondition(ctx context.Context, pipeline *tel func (r *Reconciler) setFluentBitConfigGeneratedCondition(ctx context.Context, pipeline *telemetryv1alpha1.LogPipeline) { status := metav1.ConditionTrue reason := conditions.ReasonConfigurationGenerated - + certValidationResult := getTLSCertValidationResult(ctx, pipeline, r.tlsCertValidator, r.Client) if secretref.ReferencesNonExistentSecret(ctx, r.Client, pipeline) { status = metav1.ConditionFalse reason = conditions.ReasonReferencedSecretMissing @@ -104,11 +107,40 @@ func (r *Reconciler) setFluentBitConfigGeneratedCondition(ctx context.Context, p reason = conditions.ReasonUnsupportedLokiOutput } + message := conditions.MessageForLogPipeline(reason) + + if !certValidationResult.CertValid { + status = metav1.ConditionFalse + reason = conditions.ReasonTLSCertificateInvalid + message = fmt.Sprintf(conditions.MessageForLogPipeline(reason), certValidationResult.CertValidationMessage) + } + + if !certValidationResult.PrivateKeyValid { + status = metav1.ConditionFalse + reason = conditions.ReasonTLSPrivateKeyInvalid + message = fmt.Sprintf(conditions.MessageForLogPipeline(reason), certValidationResult.PrivateKeyValidationMessage) + } + + if time.Now().After(certValidationResult.Validity) { + status = metav1.ConditionFalse + reason = conditions.ReasonTLSCertificateExpired + } + + //ensure not expired and about to expire + validUntil := time.Until(certValidationResult.Validity) + if validUntil > 0 && validUntil <= twoWeeks { + status = metav1.ConditionTrue + reason = conditions.ReasonTLSCertificateAboutToExpire + } + + if reason == conditions.ReasonTLSCertificateAboutToExpire || reason == conditions.ReasonTLSCertificateExpired { + message = fmt.Sprintf(message, certValidationResult.Validity.Format(time.DateOnly)) + } condition := metav1.Condition{ Type: conditions.TypeConfigurationGenerated, Status: status, Reason: reason, - Message: conditions.MessageForLogPipeline(reason), + Message: message, ObservedGeneration: pipeline.Generation, } diff --git a/internal/reconciler/telemetry/log_components_checker.go b/internal/reconciler/telemetry/log_components_checker.go index 9d441ceeb..870870a22 100644 --- a/internal/reconciler/telemetry/log_components_checker.go +++ b/internal/reconciler/telemetry/log_components_checker.go @@ -69,7 +69,7 @@ func (l *logComponentsChecker) firstUnhealthyPipelineReason(pipelines []telemetr for _, condType := range condTypes { for _, pipeline := range pipelines { cond := meta.FindStatusCondition(pipeline.Status.Conditions, condType) - if cond != nil && cond.Status == metav1.ConditionFalse { + if cond != nil && (cond.Status == metav1.ConditionFalse || cond.Reason == conditions.ReasonTLSCertificateAboutToExpire) { return cond.Reason } } @@ -78,13 +78,18 @@ func (l *logComponentsChecker) firstUnhealthyPipelineReason(pipelines []telemetr } func (l *logComponentsChecker) determineConditionStatus(reason string) metav1.ConditionStatus { - if reason == conditions.ReasonNoPipelineDeployed || reason == conditions.ReasonLogComponentsRunning { + if reason == conditions.ReasonNoPipelineDeployed || reason == conditions.ReasonLogComponentsRunning || reason == conditions.ReasonTLSCertificateAboutToExpire { return metav1.ConditionTrue } return metav1.ConditionFalse } func (l *logComponentsChecker) createMessageForReason(pipelines []telemetryv1alpha1.LogPipeline, parsers []telemetryv1alpha1.LogParser, reason string) string { + tlsAboutExpireMassage := determineFormattedTLSCertificateMessage(pipelines) + if len(tlsAboutExpireMassage) > 0 { + return tlsAboutExpireMassage + } + if reason != conditions.ReasonResourceBlocksDeletion { return conditions.MessageForLogPipeline(reason) } @@ -102,6 +107,20 @@ func (l *logComponentsChecker) createMessageForReason(pipelines []telemetryv1alp }) } +func determineFormattedTLSCertificateMessage(pipelines []telemetryv1alpha1.LogPipeline) string { + + for _, p := range pipelines { + cond := meta.FindStatusCondition(p.Status.Conditions, conditions.TypeConfigurationGenerated) + if cond != nil && (cond.Reason == conditions.ReasonTLSCertificateAboutToExpire || + cond.Reason == conditions.ReasonTLSCertificateExpired || + cond.Reason == conditions.ReasonTLSCertificateInvalid || + cond.Reason == conditions.ReasonTLSPrivateKeyInvalid) { + return cond.Message + } + } + return "" +} + func (l *logComponentsChecker) addReasonPrefix(reason string) string { switch { case reason == conditions.ReasonDaemonSetNotReady: diff --git a/internal/reconciler/telemetry/status.go b/internal/reconciler/telemetry/status.go index 43b791e07..231105357 100644 --- a/internal/reconciler/telemetry/status.go +++ b/internal/reconciler/telemetry/status.go @@ -71,9 +71,10 @@ func (r *Reconciler) updateOverallState(ctx context.Context, telemetry *operator } // Since LogPipeline, MetricPipeline, and TracePipeline have status conditions with positive polarity, - // we can assume that the Telemetry Module is in the 'Ready' state if all conditions of dependent resources have the status 'True.' + // we can assume that the Telemetry Module is in the 'Ready' state if all conditions of dependent resources have the status 'True', + //with the exception being the imminent expiration of the configured TLS certificate. if slices.ContainsFunc(telemetry.Status.Conditions, func(cond metav1.Condition) bool { - return cond.Status == metav1.ConditionFalse + return cond.Status == metav1.ConditionFalse || cond.Reason == conditions.ReasonTLSCertificateAboutToExpire }) { telemetry.Status.State = operatorv1alpha1.StateWarning } else { diff --git a/internal/tls/cert/tls_cert_validator.go b/internal/tls/cert/tls_cert_validator.go new file mode 100644 index 000000000..984f1d9f3 --- /dev/null +++ b/internal/tls/cert/tls_cert_validator.go @@ -0,0 +1,72 @@ +package cert + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + _ "crypto" +) + +type TLSCertValidator struct { +} + +type TLSCertValidationResult struct { + CertValid bool + CertValidationMessage string + PrivateKeyValid bool + PrivateKeyValidationMessage string + Validity time.Time +} + +func (tcv *TLSCertValidator) ValidateCertificate(certPEM []byte, keyPEM []byte) TLSCertValidationResult { + result := TLSCertValidationResult{ + CertValid: true, + PrivateKeyValid: true, + Validity: time.Now().Add(time.Hour * 24 * 365), + } + + // Parse the certificate + cert, err := parseCertificate(certPEM) + if err != nil { + result.CertValid = false + result.CertValidationMessage = err.Error() + } + + // Parse the private key + if _, err := parsePrivateKey(keyPEM); err != nil { + result.PrivateKeyValid = false + result.PrivateKeyValidationMessage = err.Error() + } + + if result.CertValid && result.PrivateKeyValid { + result.Validity = cert.NotAfter + } + + return result +} + +func parseCertificate(certPEM []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(certPEM) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block containing certificate") + } + return x509.ParseCertificate(block.Bytes) +} + +func parsePrivateKey(keyPEM []byte) (interface{}, error) { + block, _ := pem.Decode(keyPEM) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block containing private key") + } + // try to parse as PKCS8 / PRIVATE KEY + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + + // try to parse as PKCS1 / RSA PRIVATE KEY + if err != nil { + return x509.ParsePKCS1PrivateKey(block.Bytes) + } + + return privateKey, nil +} diff --git a/internal/tls/cert/tls_cert_validator_test.go b/internal/tls/cert/tls_cert_validator_test.go new file mode 100644 index 000000000..8225ec17d --- /dev/null +++ b/internal/tls/cert/tls_cert_validator_test.go @@ -0,0 +1,175 @@ +package cert + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestExpiredCertificate(t *testing.T) { + + certData := []byte(`-----BEGIN CERTIFICATE----- +MIICNjCCAZ+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADA4MQswCQYDVQQGEwJ1czEL +MAkGA1UECAwCTlkxDTALBgNVBAoMBFRlc3QxDTALBgNVBAMMBFRlc3QwHhcNMjQw +MzIxMTQyNDE0WhcNMjQwMzE5MTQyNDE0WjA4MQswCQYDVQQGEwJ1czELMAkGA1UE +CAwCTlkxDTALBgNVBAoMBFRlc3QxDTALBgNVBAMMBFRlc3QwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAMfSQ/2hwo2Qf5wA5OQ/aFuz/tFbmxwWrxtw1cAG43A9 +zG7W75kESVdTiBeKTZRXhiG0+hCa7jKULD5GWczhkwR0wepkJ+LN7SO+XDjT2YX0 +hGLfdL8opWn59d/b/0wtE7lz2Q+G/puXlDd85kM9oV+kK8oU74pZ0sNgE5lPd8t9 +AgMBAAGjUDBOMB0GA1UdDgQWBBQnFMbU0Hpg5rOfpn66vG6JVp4uXzAfBgNVHSME +GDAWgBQnFMbU0Hpg5rOfpn66vG6JVp4uXzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBDQUAA4GBAGl/tj0QW096fknAer/Q2Hmt6KINFjk6tKfnnJYYU22NMp2DQMWB +7mNxmglynPG/0hOw6OpG0ji+yPCPiZ+/RscNWgrCNAUxvsxrT8t0mEPR9lhLmxlV +WxZIBPi0z6MoiZxVKSY8EBeVYCHWS9A2l1J6gAHptihe7y1j8I2ffSHm +-----END CERTIFICATE-----`) + + keyData := []byte(`-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMfSQ/2hwo2Qf5wA +5OQ/aFuz/tFbmxwWrxtw1cAG43A9zG7W75kESVdTiBeKTZRXhiG0+hCa7jKULD5G +WczhkwR0wepkJ+LN7SO+XDjT2YX0hGLfdL8opWn59d/b/0wtE7lz2Q+G/puXlDd8 +5kM9oV+kK8oU74pZ0sNgE5lPd8t9AgMBAAECgYB5C3KMbjUAtIvY4OHHMnHxOzQd +drSba1Jf+RZC4Old0NHKQwGZW/NhpwRF3k3okqx6NrtU28V3djLm9o7nga4gbgaj +DIHVVVBBLhPS75aHaaqrol2rL0GuQtymJ9OFjFcnVY4ylU1eOD7Vvdzpgn7VtK47 +vvD1uAGypMwma1jOAQJBAPTq13sY+OtHxBSeHRkMyFshjGCc42ES3CclS6i0FiW+ +Ns2lQie+VD+chmE0OzkGdRk3IPmzfRyPAGfYBzyWr6ECQQDQ3QZ1KZ+u3kij6CUl +6RgU0fKaiXZT9e0nEC3StlkiaaGfYgyLIEWoGdr3aaiwcFsOlH/1UEuaBY52weHU +kT5dAkEA5ZpPfkBwAypZYTbFcplwLzbpQh1ycKvcpfopzrNdW+7Rs8JsnZOpqaTU +ucXci15JYuUyzcR90sshBzkXt65QYQJAcHbjWEk+c7G7mY6SGjTGQ8e9A5uLPLCK +r2MV2YVYv5/zaFgqeuu4tkid0GVzcPY/Ab3SnOxMmTXuvWGu0YAX/QJAZwN4lwdO +ga5H3f7hUBINasQIdOGEAy3clqCBpLj2eUMXHHNxVsVGBnJOEqckn6fg6pcHnhmK +5VAuzWx+wV5WwQ== +-----END PRIVATE KEY-----`) + + validator := TLSCertValidator{} + + validationResult := validator.ValidateCertificate(certData, keyData) + + require.True(t, time.Now().After(validationResult.Validity), "Certificate is not expired") +} + +func TestCertificateAndPrivateKeyValidity(t *testing.T) { + + certData := []byte(`-----BEGIN CERTIFICATE----- +MIICNjCCAZ+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADA4MQswCQYDVQQGEwJ1czEL +MAkGA1UECAwCTlkxDTALBgNVBAoMBFRlc3QxDTALBgNVBAMMBFRlc3QwHhcNMjQw +MzIxMTQyNDE0WhcNMjQwMzE5MTQyNDE0WjA4MQswCQYDVQQGEwJ1czELMAkGA1UE +CAwCTlkxDTALBgNVBAoMBFRlc3QxDTALBgNVBAMMBFRlc3QwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAMfSQ/2hwo2Qf5wA5OQ/aFuz/tFbmxwWrxtw1cAG43A9 +zG7W75kESVdTiBeKTZRXhiG0+hCa7jKULD5GWczhkwR0wepkJ+LN7SO+XDjT2YX0 +hGLfdL8opWn59d/b/0wtE7lz2Q+G/puXlDd85kM9oV+kK8oU74pZ0sNgE5lPd8t9 +AgMBAAGjUDBOMB0GA1UdDgQWBBQnFMbU0Hpg5rOfpn66vG6JVp4uXzAfBgNVHSME +GDAWgBQnFMbU0Hpg5rOfpn66vG6JVp4uXzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBDQUAA4GBAGl/tj0QW096fknAer/Q2Hmt6KINFjk6tKfnnJYYU22NMp2DQMWB +7mNxmglynPG/0hOw6OpG0ji+yPCPiZ+/RscNWgrCNAUxvsxrT8t0mEPR9lhLmxlV +WxZIBPi0z6MoiZxVKSY8EBeVYCHWS9A2l1J6gAHptihe7y1j8I2ffSHm +-----END CERTIFICATE-----`) + + keyData := []byte(`-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMfSQ/2hwo2Qf5wA +5OQ/aFuz/tFbmxwWrxtw1cAG43A9zG7W75kESVdTiBeKTZRXhiG0+hCa7jKULD5G +WczhkwR0wepkJ+LN7SO+XDjT2YX0hGLfdL8opWn59d/b/0wtE7lz2Q+G/puXlDd8 +5kM9oV+kK8oU74pZ0sNgE5lPd8t9AgMBAAECgYB5C3KMbjUAtIvY4OHHMnHxOzQd +drSba1Jf+RZC4Old0NHKQwGZW/NhpwRF3k3okqx6NrtU28V3djLm9o7nga4gbgaj +DIHVVVBBLhPS75aHaaqrol2rL0GuQtymJ9OFjFcnVY4ylU1eOD7Vvdzpgn7VtK47 +vvD1uAGypMwma1jOAQJBAPTq13sY+OtHxBSeHRkMyFshjGCc42ES3CclS6i0FiW+ +Ns2lQie+VD+chmE0OzkGdRk3IPmzfRyPAGfYBzyWr6ECQQDQ3QZ1KZ+u3kij6CUl +6RgU0fKaiXZT9e0nEC3StlkiaaGfYgyLIEWoGdr3aaiwcFsOlH/1UEuaBY52weHU +kT5dAkEA5ZpPfkBwAypZYTbFcplwLzbpQh1ycKvcpfopzrNdW+7Rs8JsnZOpqaTU +ucXci15JYuUyzcR90sshBzkXt65QYQJAcHbjWEk+c7G7mY6SGjTGQ8e9A5uLPLCK +r2MV2YVYv5/zaFgqeuu4tkid0GVzcPY/Ab3SnOxMmTXuvWGu0YAX/QJAZwN4lwdO +ga5H3f7hUBINasQIdOGEAy3clqCBpLj2eUMXHHNxVsVGBnJOEqckn6fg6pcHnhmK +5VAuzWx+wV5WwQ== +-----END PRIVATE KEY-----`) + + validator := TLSCertValidator{} + + validationResult := validator.ValidateCertificate(certData, keyData) + + require.True(t, validationResult.CertValid, "Certificate is not valid") + require.True(t, validationResult.PrivateKeyValid, "Private Key is not valid") +} + +func TestInvalidCertificate(t *testing.T) { + + certData := []byte(`-----BEGIN CERTIFICATE----- +MIICNjCCAZ+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADA4MQswCQYDVQQGEwJ1czEL +MAkGA1UECAwCTlkxDTALBgNVBAoMBFRlc3QxDTALBgNVBAMMBFRlc3QwHhcNMjQw +MzIxMTQyNDE0WhcNMjQwMzE5MTQyNDE0WjA4MQswCQYDVQQGEwJ1czELMAkGA1UE +CAwCTlkxDTALBgNVBAoMBFRlc3QxDTALBgNVBAMMBFRlc3QwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAMfSQ/2hwo2Qf5wA5OQ/aFuz/tFbmxwWrxtw1cAG43A9 +zG7W75kESVdTiBeKTZRXhiG0+hCa7jKULD5GWczhkwR0wepkJ+LN7SO+XDjT2YX0 +hGLfdL8opWn59d/b/0wtE7lz2Q+G/puXlDd85kM9oV+kK8oU74pZ0sNgE5lPd8t9 +AgMBAAGjUDBOMB0GA1UdDgQWBBQnFMbU0Hpg5rOfpn66vG6JVp4uXzAfBgNVHSME +GDAWgBQnFMbU0Hpg5rOfpn66vG6JVp4uXzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBDQUAA4GBAGl/tj0QW096fknAer/Q2Hmt6KINFjk6tKfnnJYYU22NMp2DQMWB +7mNxmglynPG/0hOw6OpG0ji+yPCPiZ+/RscNWgrCNAUxvsxrT8t0mEPR9lhLmxlV +WxZIBPi0z6MoiZxVKSY8EBeVYCHWS9A2l1J6gAHptihe7y1j8I2ffS +-----END CERTIFICATE-----`) + + keyData := []byte(`-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMfSQ/2hwo2Qf5wA +5OQ/aFuz/tFbmxwWrxtw1cAG43A9zG7W75kESVdTiBeKTZRXhiG0+hCa7jKULD5G +WczhkwR0wepkJ+LN7SO+XDjT2YX0hGLfdL8opWn59d/b/0wtE7lz2Q+G/puXlDd8 +5kM9oV+kK8oU74pZ0sNgE5lPd8t9AgMBAAECgYB5C3KMbjUAtIvY4OHHMnHxOzQd +drSba1Jf+RZC4Old0NHKQwGZW/NhpwRF3k3okqx6NrtU28V3djLm9o7nga4gbgaj +DIHVVVBBLhPS75aHaaqrol2rL0GuQtymJ9OFjFcnVY4ylU1eOD7Vvdzpgn7VtK47 +vvD1uAGypMwma1jOAQJBAPTq13sY+OtHxBSeHRkMyFshjGCc42ES3CclS6i0FiW+ +Ns2lQie+VD+chmE0OzkGdRk3IPmzfRyPAGfYBzyWr6ECQQDQ3QZ1KZ+u3kij6CUl +6RgU0fKaiXZT9e0nEC3StlkiaaGfYgyLIEWoGdr3aaiwcFsOlH/1UEuaBY52weHU +kT5dAkEA5ZpPfkBwAypZYTbFcplwLzbpQh1ycKvcpfopzrNdW+7Rs8JsnZOpqaTU +ucXci15JYuUyzcR90sshBzkXt65QYQJAcHbjWEk+c7G7mY6SGjTGQ8e9A5uLPLCK +r2MV2YVYv5/zaFgqeuu4tkid0GVzcPY/Ab3SnOxMmTXuvWGu0YAX/QJAZwN4lwdO +ga5H3f7hUBINasQIdOGEAy3clqCBpLj2eUMXHHNxVsVGBnJOEqckn6fg6pcHnhmK +5VAuzWx+wV5WwQ== +-----END PRIVATE KEY-----`) + + validator := TLSCertValidator{} + + validationResult := validator.ValidateCertificate(certData, keyData) + + require.False(t, validationResult.CertValid, "Certificate is valid") + require.True(t, validationResult.PrivateKeyValid, "Private Key is not valid") +} + +func TestInvalidPrivateKey(t *testing.T) { + + certData := []byte(`-----BEGIN CERTIFICATE----- +MIICNjCCAZ+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADA4MQswCQYDVQQGEwJ1czEL +MAkGA1UECAwCTlkxDTALBgNVBAoMBFRlc3QxDTALBgNVBAMMBFRlc3QwHhcNMjQw +MzIxMTQyNDE0WhcNMjQwMzE5MTQyNDE0WjA4MQswCQYDVQQGEwJ1czELMAkGA1UE +CAwCTlkxDTALBgNVBAoMBFRlc3QxDTALBgNVBAMMBFRlc3QwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAMfSQ/2hwo2Qf5wA5OQ/aFuz/tFbmxwWrxtw1cAG43A9 +zG7W75kESVdTiBeKTZRXhiG0+hCa7jKULD5GWczhkwR0wepkJ+LN7SO+XDjT2YX0 +hGLfdL8opWn59d/b/0wtE7lz2Q+G/puXlDd85kM9oV+kK8oU74pZ0sNgE5lPd8t9 +AgMBAAGjUDBOMB0GA1UdDgQWBBQnFMbU0Hpg5rOfpn66vG6JVp4uXzAfBgNVHSME +GDAWgBQnFMbU0Hpg5rOfpn66vG6JVp4uXzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBDQUAA4GBAGl/tj0QW096fknAer/Q2Hmt6KINFjk6tKfnnJYYU22NMp2DQMWB +7mNxmglynPG/0hOw6OpG0ji+yPCPiZ+/RscNWgrCNAUxvsxrT8t0mEPR9lhLmxlV +WxZIBPi0z6MoiZxVKSY8EBeVYCHWS9A2l1J6gAHptihe7y1j8I2ffSHm +-----END CERTIFICATE-----`) + + keyData := []byte(`-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMfSQ/2hwo2Qf5wA +5OQ/aFuz/tFbmxwWrxtw1cAG43A9zG7W75kESVdTiBeKTZRXhiG0+hCa7jKULD5G +WczhkwR0wepkJ+LN7SO+XDjT2YX0hGLfdL8opWn59d/b/0wtE7lz2Q+G/puXlDd8 +5kM9oV+kK8oU74pZ0sNgE5lPd8t9AgMBAAECgYB5C3KMbjUAtIvY4OHHMnHxOzQd +drSba1Jf+RZC4Old0NHKQwGZW/NhpwRF3k3okqx6NrtU28V3djLm9o7nga4gbgaj +DIHVVVBBLhPS75aHaaqrol2rL0GuQtymJ9OFjFcnVY4ylU1eOD7Vvdzpgn7VtK47 +vvD1uAGypMwma1jOAQJBAPTq13sY+OtHxBSeHRkMyFshjGCc42ES3CclS6i0FiW+ +Ns2lQie+VD+chmE0OzkGdRk3IPmzfRyPAGfYBzyWr6ECQQDQ3QZ1KZ+u3kij6CUl +6RgU0fKaiXZT9e0nEC3StlkiaaGfYgyLIEWoGdr3aaiwcFsOlH/1UEuaBY52weHU +kT5dAkEA5ZpPfkBwAypZYTbFcplwLzbpQh1ycKvcpfopzrNdW+7Rs8JsnZOpqaTU +ucXci15JYuUyzcR90sshBzkXt65QYQJAcHbjWEk+c7G7mY6SGjTGQ8e9A5uLPLCK +r2MV2YVYv5/zaFgqeuu4tkid0GVzcPY/Ab3SnOxMmTXuvWGu0YAX/QJAZwN4lwdO +ga5H3f7hUBINasQIdOGEAy3clqCBpLj2eUMXHHNxVsVGBnJOEqckn6fg6pcHnhmK +5VAuzWx+wWwQ== +-----END PRIVATE KEY-----`) + + validator := TLSCertValidator{} + + validationResult := validator.ValidateCertificate(certData, keyData) + + require.True(t, validationResult.CertValid, "Certificate is not valid") + require.False(t, validationResult.PrivateKeyValid, "Private Key is valid") +}