From 1bb0c6feccdfc5d1478f9e5e0cec3a55c83797b7 Mon Sep 17 00:00:00 2001 From: Yaroslav Borbat Date: Mon, 19 Jan 2026 18:13:57 +0300 Subject: [PATCH] Reissue the certificates if their validity period differs from the expected one Signed-off-by: Yaroslav Borbat --- common-hooks/tls-certificate/internal_tls.go | 22 +- .../tls-certificate/internal_tls_test.go | 202 ++++++++++++++++++ 2 files changed, 217 insertions(+), 7 deletions(-) diff --git a/common-hooks/tls-certificate/internal_tls.go b/common-hooks/tls-certificate/internal_tls.go index 15c35b0..973406a 100644 --- a/common-hooks/tls-certificate/internal_tls.go +++ b/common-hooks/tls-certificate/internal_tls.go @@ -281,7 +281,7 @@ func GenSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i // 2.2) save new common ca in values // 2.3) mark certificates to regenerate if useCommonCA { - auth, err = getCommonCA(input, conf.CommonCAPath(), caOutdatedDuration) + auth, err = getCommonCA(input, conf.CommonCAPath(), caExpiryDuration, caOutdatedDuration) if err != nil { commonCACanonicalName := conf.CommonCACanonicalName @@ -320,7 +320,7 @@ func GenSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i // update certificate if less than 6 month left. We create certificate for 10 years, so it looks acceptable // and we don't need to create Crontab schedule - caOutdated, err := isOutdatedCA(cert.CA, caOutdatedDuration) + caOutdated, err := isOutdatedCA(cert.CA, caExpiryDuration, caOutdatedDuration) if err != nil { input.Logger.Warn("is outdated ca", log.Err(err)) } @@ -332,7 +332,7 @@ func GenSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i caOutdated = true } - certOutdated, err := isIrrelevantCert(cert.Cert, sans, certOutdatedDuration) + certOutdated, err := isIrrelevantCert(cert.Cert, sans, certExpiryDuration, certOutdatedDuration) if err != nil { input.Logger.Warn("is irrelevant cert", log.Err(err)) } @@ -384,7 +384,7 @@ func convCertToValues(cert *certificate.Certificate) CertValues { var ErrCertificateIsNotFound = errors.New("certificate is not found") var ErrCAIsInvalidOrOutdated = errors.New("ca is invalid or outdated") -func getCommonCA(input *pkg.HookInput, valKey string, caOutdatedDuration time.Duration) (*certificate.Authority, error) { +func getCommonCA(input *pkg.HookInput, valKey string, caExpiryDuration, caOutdatedDuration time.Duration) (*certificate.Authority, error) { auth := new(certificate.Authority) ca, ok := input.Values.GetOk(valKey) @@ -397,7 +397,7 @@ func getCommonCA(input *pkg.HookInput, valKey string, caOutdatedDuration time.Du return nil, err } - outdated, err := isOutdatedCA(auth.Cert, caOutdatedDuration) + outdated, err := isOutdatedCA(auth.Cert, caExpiryDuration, caOutdatedDuration) if err != nil { input.Logger.Error("is outdated ca", log.Err(err)) @@ -446,12 +446,16 @@ func generateNewSelfSignedTLS(input SelfSignedCertValues) (*certificate.Certific } // check certificate duration and SANs list -func isIrrelevantCert(certData []byte, desiredSANSs []string, certOutdatedDuration time.Duration) (bool, error) { +func isIrrelevantCert(certData []byte, desiredSANSs []string, certExpiryDuration, certOutdatedDuration time.Duration) (bool, error) { cert, err := certificate.ParseCertificate(certData) if err != nil { return false, fmt.Errorf("parse certificate: %w", err) } + if cert.NotAfter.Sub(cert.NotBefore) != certExpiryDuration { + return true, nil + } + if time.Until(cert.NotAfter) < certOutdatedDuration { return true, nil } @@ -483,7 +487,7 @@ func isIrrelevantCert(certData []byte, desiredSANSs []string, certOutdatedDurati return false, nil } -func isOutdatedCA(ca []byte, caOutdatedDuration time.Duration) (bool, error) { +func isOutdatedCA(ca []byte, caExpiryDuration, caOutdatedDuration time.Duration) (bool, error) { // Issue a new certificate if there is no CA in the secret. // Without CA it is not possible to validate the certificate. if len(ca) == 0 { @@ -495,6 +499,10 @@ func isOutdatedCA(ca []byte, caOutdatedDuration time.Duration) (bool, error) { return false, fmt.Errorf("parse certificate: %w", err) } + if cert.NotAfter.Sub(cert.NotBefore) != caExpiryDuration { + return true, nil + } + if time.Until(cert.NotAfter) < caOutdatedDuration { return true, nil } diff --git a/common-hooks/tls-certificate/internal_tls_test.go b/common-hooks/tls-certificate/internal_tls_test.go index a396f71..7ed07bb 100644 --- a/common-hooks/tls-certificate/internal_tls_test.go +++ b/common-hooks/tls-certificate/internal_tls_test.go @@ -445,4 +445,206 @@ func Test_GenSelfSignedTLS(t *testing.T) { err := tlscertificate.GenSelfSignedTLS(config)(context.Background(), input) assert.NoError(t, err) }) + + t.Run("wrong ca expiry duration in snapshot", func(t *testing.T) { + dc := mock.NewDependencyContainerMock(t) + + snapshots := mock.NewSnapshotsMock(t) + snapshots.GetMock.When(tlscertificate.InternalTLSSnapshotKey).Then( + []pkg.Snapshot{ + mock.NewSnapshotMock(t).UnmarshalToMock.Set(func(v any) error { + ca, err := certificate.GenerateCA( + "cert-name", + certificate.WithKeyAlgo("ecdsa"), + certificate.WithKeySize(256), + certificate.WithCAExpiry((24*time.Hour)*365*10)) + + assert.NoError(t, err) + + cert, err := certificate.GenerateSelfSignedCert( + "cert-name", + ca, + certificate.WithSANs([]string{ + "example-webhook", + "example-webhook.d8-example-module", + "example-webhook.d8-example-module.svc", + "example-webhook.d8-example-module.svc.cluster.local", + "example-webhook.d8-example-module.svc.127.0.0.1.sslip.io", + }...), + certificate.WithKeyAlgo("ecdsa"), + certificate.WithKeySize(256), + certificate.WithSigningDefaultExpiry((24*time.Hour)*365*10), + certificate.WithSigningDefaultUsage([]string{ + "signing", + "key encipherment", + "requestheader-client", + }), + ) + + assert.NoError(t, err) + + value := v.(*certificate.Certificate) + *value = *cert + + return nil + }), + }, + ) + + values := mock.NewOutputPatchableValuesCollectorMock(t) + + values.GetMock.When("global.discovery.clusterDomain").Then(gjson.Result{Type: gjson.String, Str: "cluster.local"}) + values.GetMock.When("global.modules.publicDomainTemplate").Then(gjson.Result{Type: gjson.String, Str: "%s.127.0.0.1.sslip.io"}) + + values.SetMock.Set(func(path string, v any) { + assert.Equal(t, "d8-example-module.internal.webhookCert", path) + + values, ok := v.(tlscertificate.CertValues) + assert.True(t, ok) + + assert.NotEmpty(t, values.CA) + assert.NotEmpty(t, values.Crt) + assert.NotEmpty(t, values.Key) + + ca, err := certificate.ParseCertificate([]byte(values.CA)) + assert.NoError(t, err) + assert.Equal(t, ca.NotAfter.Sub(ca.NotBefore), (24*time.Hour)*365) + + cert, err := certificate.ParseCertificate([]byte(values.Crt)) + assert.NoError(t, err) + + assert.Equal(t, []string{ + "example-webhook", + "example-webhook.d8-example-module", + "example-webhook.d8-example-module.svc", + "example-webhook.d8-example-module.svc.cluster.local", + "example-webhook.d8-example-module.svc.127.0.0.1.sslip.io", + }, cert.DNSNames) + }) + + var input = &pkg.HookInput{ + Snapshots: snapshots, + Values: values, + DC: dc, + Logger: log.NewNop(), + } + + config := tlscertificate.GenSelfSignedTLSHookConf{ + CN: "cert-name", + TLSSecretName: "secret-webhook-cert", + Namespace: "some-namespace", + SANs: tlscertificate.DefaultSANs([]string{ + "example-webhook", + "example-webhook.d8-example-module", + "example-webhook.d8-example-module.svc", + "%CLUSTER_DOMAIN%://example-webhook.d8-example-module.svc", + "%PUBLIC_DOMAIN%://example-webhook.d8-example-module.svc", + }), + FullValuesPathPrefix: "d8-example-module.internal.webhookCert", + CAExpiryDuration: (24 * time.Hour) * 365, + } + + err := tlscertificate.GenSelfSignedTLS(config)(context.Background(), input) + assert.NoError(t, err) + }) + + t.Run("wrong certificate expiry duration in snapshot", func(t *testing.T) { + dc := mock.NewDependencyContainerMock(t) + + snapshots := mock.NewSnapshotsMock(t) + snapshots.GetMock.When(tlscertificate.InternalTLSSnapshotKey).Then( + []pkg.Snapshot{ + mock.NewSnapshotMock(t).UnmarshalToMock.Set(func(v any) error { + ca, err := certificate.GenerateCA( + "cert-name", + certificate.WithKeyAlgo("ecdsa"), + certificate.WithKeySize(256), + certificate.WithCAExpiry((24*time.Hour)*365*10)) + + assert.NoError(t, err) + + cert, err := certificate.GenerateSelfSignedCert( + "cert-name", + ca, + certificate.WithSANs([]string{ + "example-webhook", + "example-webhook.d8-example-module", + "example-webhook.d8-example-module.svc", + "example-webhook.d8-example-module.svc.cluster.local", + "example-webhook.d8-example-module.svc.127.0.0.1.sslip.io", + }...), + certificate.WithKeyAlgo("ecdsa"), + certificate.WithKeySize(256), + certificate.WithSigningDefaultExpiry((24*time.Hour)*365*10), + certificate.WithSigningDefaultUsage([]string{ + "signing", + "key encipherment", + "requestheader-client", + }), + ) + + assert.NoError(t, err) + + value := v.(*certificate.Certificate) + *value = *cert + + return nil + }), + }, + ) + + values := mock.NewOutputPatchableValuesCollectorMock(t) + + values.GetMock.When("global.discovery.clusterDomain").Then(gjson.Result{Type: gjson.String, Str: "cluster.local"}) + values.GetMock.When("global.modules.publicDomainTemplate").Then(gjson.Result{Type: gjson.String, Str: "%s.127.0.0.1.sslip.io"}) + + values.SetMock.Set(func(path string, v any) { + assert.Equal(t, "d8-example-module.internal.webhookCert", path) + + values, ok := v.(tlscertificate.CertValues) + assert.True(t, ok) + + assert.NotEmpty(t, values.CA) + assert.NotEmpty(t, values.Crt) + assert.NotEmpty(t, values.Key) + + cert, err := certificate.ParseCertificate([]byte(values.Crt)) + assert.NoError(t, err) + + assert.Equal(t, []string{ + "example-webhook", + "example-webhook.d8-example-module", + "example-webhook.d8-example-module.svc", + "example-webhook.d8-example-module.svc.cluster.local", + "example-webhook.d8-example-module.svc.127.0.0.1.sslip.io", + }, cert.DNSNames) + + assert.Equal(t, cert.NotAfter.Sub(cert.NotBefore), (24*time.Hour)*365) + }) + + var input = &pkg.HookInput{ + Snapshots: snapshots, + Values: values, + DC: dc, + Logger: log.NewNop(), + } + + config := tlscertificate.GenSelfSignedTLSHookConf{ + CN: "cert-name", + TLSSecretName: "secret-webhook-cert", + Namespace: "some-namespace", + SANs: tlscertificate.DefaultSANs([]string{ + "example-webhook", + "example-webhook.d8-example-module", + "example-webhook.d8-example-module.svc", + "%CLUSTER_DOMAIN%://example-webhook.d8-example-module.svc", + "%PUBLIC_DOMAIN%://example-webhook.d8-example-module.svc", + }), + FullValuesPathPrefix: "d8-example-module.internal.webhookCert", + CertExpiryDuration: (24 * time.Hour) * 365, + } + + err := tlscertificate.GenSelfSignedTLS(config)(context.Background(), input) + assert.NoError(t, err) + }) }