diff --git a/common-hooks/tls-certificate/internal_tls.go b/common-hooks/tls-certificate/internal_tls.go index 73683a6..c5a2441 100644 --- a/common-hooks/tls-certificate/internal_tls.go +++ b/common-hooks/tls-certificate/internal_tls.go @@ -2,13 +2,17 @@ package tlscertificate import ( "context" + "encoding/json" + "errors" "fmt" + "slices" "strings" "time" certificatesv1 "k8s.io/api/certificates/v1" "k8s.io/utils/net" + "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/pkg/certificate" objectpatch "github.com/deckhouse/module-sdk/pkg/object-patch" @@ -24,6 +28,7 @@ const ( keyAlgorithm = "ecdsa" keySize = 256 SnapshotKey = "secret" + CommonCAKey = "commonSelfsignedCa" ) type GenSelfSignedTLSHookConf struct { @@ -64,12 +69,29 @@ type GenSelfSignedTLSHookConf struct { // if return value is false - hook will stop its execution // if return value is true - hook will continue BeforeHookCheck func(input *pkg.HookInput) bool + + // CommonCA option toggle using common ca, you can pass your ca with values + // full path will be + // FullValuesPathPrefix + CommonCAKey + // Example: FullValuesPathPrefix = 'prometheusMetricsAdapter.internal.adapter' + // Values to store: + // prometheusMetricsAdapter.internal.adapter.commonSelfsignedCa.key + // prometheusMetricsAdapter.internal.adapter.commonSelfsignedCa.crt + // Data in values store as plain text + // In helm templates you need use `b64enc` function to encode + CommonCA bool } func (gss GenSelfSignedTLSHookConf) Path() string { return strings.TrimSuffix(gss.FullValuesPathPrefix, ".") } +func (gss GenSelfSignedTLSHookConf) CommonCAPath() string { + path := strings.Join([]string{gss.FullValuesPathPrefix, CommonCAKey}, ".") + + return strings.TrimSuffix(path, ".") +} + // SANsGenerator function for generating sans type SANsGenerator func(input *pkg.HookInput) []string @@ -141,7 +163,7 @@ func genSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i } } - var cert certificate.Certificate + var cert *certificate.Certificate cn, sans := conf.CN, conf.SANs(input) @@ -150,36 +172,75 @@ func genSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i return fmt.Errorf("unmarshal to struct: %w", err) } - if len(certs) == 0 { - // No certificate in snapshot => generate a new one. - // Secret will be updated by Helm. - cert, err = generateNewSelfSignedTLS(input, cn, sans, usages) + var auth *certificate.Authority + + mustGenerate := false + + // 1) get and validate common ca + // 2) if not valid: + // 2.1) regenerate common ca + // 2.2) save new common ca in values + // 2.3) mark certificates to regenerate + if conf.CommonCA { + auth, err = getCommonCA(input, conf.CommonCAPath()) if err != nil { - return err + auth, err = certificate.GenerateCA(input.Logger, + cn, + certificate.WithKeyAlgo(keyAlgorithm), + certificate.WithKeySize(keySize), + certificate.WithCAExpiry(caExpiryDurationStr)) + if err != nil { + return fmt.Errorf("generate ca: %w", err) + } + + input.Values.Set(conf.CommonCAPath(), auth) + + mustGenerate = true } - } else { + } + + // if no certificate - regenerate + if len(certs) == 0 { + mustGenerate = true + } + + // 1) take first certificate + // 2) check certificate ca outdated + // 3) if using common CA - compare cert CA and common CA (if different - mark outdated) + // 4) check certificate outdated + // 5) if CA or cert outdated - regenerate + if len(certs) > 0 { // Certificate is in the snapshot => load it. - cert = certs[0] + cert = &certs[0] // 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) if err != nil { - input.Logger.Error(err.Error()) + input.Logger.Warn("is outdated ca", log.Err(err)) + } + + // if common ca and cert ca is not equal - regenerate cert + if conf.CommonCA && !slices.Equal(auth.Cert, cert.CA) { + input.Logger.Warn("common ca is not equal cert ca") + + caOutdated = true } certOutdated, err := isIrrelevantCert(cert.Cert, sans) if err != nil { - input.Logger.Error(err.Error()) + input.Logger.Warn("is irrelevant cert", log.Err(err)) } // In case of errors, both these flags are false to avoid regeneration loop for the // certificate. - if caOutdated || certOutdated { - cert, err = generateNewSelfSignedTLS(input, cn, sans, usages) - if err != nil { - return err - } + mustGenerate = caOutdated || certOutdated + } + + if mustGenerate { + cert, err = generateNewSelfSignedTLS(input, cn, auth, sans, usages) + if err != nil { + return fmt.Errorf("generate new self signed tls: %w", err) } } @@ -197,7 +258,7 @@ type certValues struct { // The certificate mapping "cert" -> "crt". We are migrating to "crt" naming for certificates // in values. -func convCertToValues(cert certificate.Certificate) certValues { +func convCertToValues(cert *certificate.Certificate) certValues { return certValues{ CA: string(cert.CA), Crt: string(cert.Cert), @@ -205,17 +266,52 @@ func convCertToValues(cert certificate.Certificate) certValues { } } -func generateNewSelfSignedTLS(input *pkg.HookInput, cn string, sans, usages []string) (certificate.Certificate, error) { - ca, err := certificate.GenerateCA(input.Logger, - cn, - certificate.WithKeyAlgo(keyAlgorithm), - certificate.WithKeySize(keySize), - certificate.WithCAExpiry(caExpiryDurationStr)) +var ErrCAIsInvalidOrOutdated = errors.New("ca is invalid or outdated") + +func getCommonCA(input *pkg.HookInput, valKey string) (*certificate.Authority, error) { + auth := new(certificate.Authority) + + ca, ok := input.Values.GetOk(valKey) + if ok { + err := json.Unmarshal([]byte(ca.String()), auth) + if err != nil { + return nil, err + } + } + + outdated, err := isOutdatedCA(auth.Cert) if err != nil { - return certificate.Certificate{}, err + input.Logger.Error("is outdated ca", log.Err(err)) + + return nil, err } - return certificate.GenerateSelfSignedCert(input.Logger, + if !outdated { + return auth, nil + } + + return nil, ErrCAIsInvalidOrOutdated +} + +// generateNewSelfSignedTLS +// +// if you pass ca - it will be used to sign new certificate +// if pass nil ca - it will be generate to sign new certificate +func generateNewSelfSignedTLS(input *pkg.HookInput, cn string, ca *certificate.Authority, sans, usages []string) (*certificate.Certificate, error) { + if ca == nil { + var err error + + ca, err = certificate.GenerateCA(input.Logger, + cn, + certificate.WithKeyAlgo(keyAlgorithm), + certificate.WithKeySize(keySize), + certificate.WithCAExpiry(caExpiryDurationStr)) + if err != nil { + return nil, fmt.Errorf("generate ca: %w", err) + } + } + + cert, err := certificate.GenerateSelfSignedCert(input.Logger, cn, ca, certificate.WithSANs(sans...), @@ -224,6 +320,12 @@ func generateNewSelfSignedTLS(input *pkg.HookInput, cn string, sans, usages []st certificate.WithSigningDefaultExpiry(certExpiryDuration), certificate.WithSigningDefaultUsage(usages), ) + + if err != nil { + return nil, fmt.Errorf("generate self signed cert: %w", err) + } + + return cert, nil } // check certificate duration and SANs list diff --git a/common-hooks/tls-certificate/order_certificate.go b/common-hooks/tls-certificate/order_certificate.go index 5f57814..4dd3771 100644 --- a/common-hooks/tls-certificate/order_certificate.go +++ b/common-hooks/tls-certificate/order_certificate.go @@ -301,7 +301,7 @@ func IssueCertificate(ctx context.Context, input *pkg.HookInput, request OrderCe ctxWTO, cancel := context.WithTimeout(context.Background(), request.WaitTimeout) defer cancel() - crtPEM, err := certificate.WaitForCertificate(ctxWTO, k8, csr.Name, csr.UID) + crtPEM, err := certificate.WaitForCertificate(ctxWTO, k8, csr.Name, csr.UID, input.Logger) if err != nil { return nil, fmt.Errorf("%s CertificateSigningRequest was not signed: %v", request.CommonName, err) } diff --git a/pkg/certificate/ca.go b/pkg/certificate/ca.go index f8b2fb3..2bd7253 100644 --- a/pkg/certificate/ca.go +++ b/pkg/certificate/ca.go @@ -31,7 +31,7 @@ type Authority struct { Cert []byte `json:"crt"` } -func GenerateCA(logger pkg.Logger, cn string, options ...Option) (Authority, error) { +func GenerateCA(logger pkg.Logger, cn string, options ...Option) (*Authority, error) { request := &csr.CertificateRequest{ CN: cn, CA: &csr.CAConfig{ @@ -59,5 +59,5 @@ func GenerateCA(logger pkg.Logger, cn string, options ...Option) (Authority, err logger.Error(buf.String()) } - return Authority{Cert: ca, Key: key}, err + return &Authority{Cert: ca, Key: key}, err } diff --git a/pkg/certificate/helpers.go b/pkg/certificate/helpers.go index 3977dba..cb67fcd 100644 --- a/pkg/certificate/helpers.go +++ b/pkg/certificate/helpers.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "log/slog" "time" "github.com/cloudflare/cfssl/helpers" @@ -33,8 +34,10 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" - "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/module-sdk/pkg" ) func IsCertificateExpiringSoon(cert []byte, durationLeft time.Duration) (bool, error) { @@ -51,7 +54,7 @@ func IsCertificateExpiringSoon(cert []byte, durationLeft time.Duration) (bool, e // modified client-go@v0.29.8/util/certificate/csr/csr.go // // WaitForCertificate waits for a certificate to be issued until timeout, or returns an error. -func WaitForCertificate(ctx context.Context, clientWOWatch client.Client, reqName string, reqUID types.UID) (certData []byte, err error) { +func WaitForCertificate(ctx context.Context, clientWOWatch client.Client, reqName string, reqUID types.UID, logger pkg.Logger) (certData []byte, err error) { c, ok := clientWOWatch.(client.WithWatch) if !ok { return nil, errors.New("client without watch") @@ -81,7 +84,7 @@ func WaitForCertificate(ctx context.Context, clientWOWatch client.Client, reqNam break } - klog.V(2).Infof("error fetching v1 certificate signing request: %v", err) + logger.Info("error fetching v1 certificate signing request", log.Err(err)) // return if we've timed out if err := ctx.Err(); err != nil { @@ -107,7 +110,7 @@ func WaitForCertificate(ctx context.Context, clientWOWatch client.Client, reqNam break } - klog.V(2).Infof("error fetching v1beta1 certificate signing request: %v", err) + logger.Info("error fetching v1beta1 certificate signing request", log.Err(err)) // return if we've timed out if err := ctx.Err(); err != nil { @@ -152,11 +155,11 @@ func WaitForCertificate(ctx context.Context, clientWOWatch client.Client, reqNam } if approved { if len(csr.Status.Certificate) > 0 { - klog.V(2).Infof("certificate signing request %s is issued", csr.Name) + logger.Info("certificate signing request is issued", slog.String("request", csr.Name)) issuedCertificate = csr.Status.Certificate return true, nil } - klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name) + logger.Info("certificate signing request is approved, waiting to be issued", slog.String("request", csr.Name)) } case *certificatesv1beta1.CertificateSigningRequest: @@ -177,11 +180,11 @@ func WaitForCertificate(ctx context.Context, clientWOWatch client.Client, reqNam } if approved { if len(csr.Status.Certificate) > 0 { - klog.V(2).Infof("certificate signing request %s is issued", csr.Name) + logger.Info("certificate signing request is issued", slog.String("request", csr.Name)) issuedCertificate = csr.Status.Certificate return true, nil } - klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name) + logger.Info("certificate signing request is approved, waiting to be issued", slog.String("request", csr.Name)) } default: diff --git a/pkg/certificate/hook_helpers.go b/pkg/certificate/hook_helpers.go index f761191..44a404b 100644 --- a/pkg/certificate/hook_helpers.go +++ b/pkg/certificate/hook_helpers.go @@ -29,7 +29,7 @@ var JQFilterApplyCaSelfSignedCert = `{ }` func GetOrCreateCa(input *pkg.HookInput, snapshotKey, cn string) (*Authority, error) { - var selfSignedCA Authority + var selfSignedCA *Authority authorities, err := objectpatch.UnmarshalToStruct[Authority](input.Snapshots, snapshotKey) if err != nil { @@ -46,5 +46,5 @@ func GetOrCreateCa(input *pkg.HookInput, snapshotKey, cn string) (*Authority, er return nil, fmt.Errorf("cannot generate selfsigned ca: %v", err) } - return &selfSignedCA, nil + return selfSignedCA, nil } diff --git a/pkg/certificate/selfsigned.go b/pkg/certificate/selfsigned.go index 9e0b206..17f63ce 100644 --- a/pkg/certificate/selfsigned.go +++ b/pkg/certificate/selfsigned.go @@ -18,6 +18,7 @@ package certificate import ( "bytes" + "errors" "log" "log/slog" "time" @@ -55,7 +56,11 @@ func WithSigningDefaultUsage(usage []string) SigningOption { } } -func GenerateSelfSignedCert(logger pkg.Logger, cn string, ca Authority, options ...interface{}) (Certificate, error) { +func GenerateSelfSignedCert(logger pkg.Logger, cn string, ca *Authority, options ...interface{}) (*Certificate, error) { + if ca == nil { + return nil, errors.New("ca is nil") + } + logger.Debug("Generate self-signed cert", slog.String("cn", cn)) request := &csr.CertificateRequest{ CN: cn, @@ -81,19 +86,19 @@ func GenerateSelfSignedCert(logger pkg.Logger, cn string, ca Authority, options g := &csr.Generator{Validator: genkey.Validator} csrBytes, key, err := g.ProcessRequest(request) if err != nil { - return Certificate{}, err + return nil, err } req := signer.SignRequest{Request: string(csrBytes)} parsedCa, err := helpers.ParseCertificatePEM(ca.Cert) if err != nil { - return Certificate{}, err + return nil, err } priv, err := helpers.ParsePrivateKeyPEM(ca.Key) if err != nil { - return Certificate{}, err + return nil, err } signingConfig := &config.Signing{ @@ -108,15 +113,15 @@ func GenerateSelfSignedCert(logger pkg.Logger, cn string, ca Authority, options s, err := local.NewSigner(priv, parsedCa, signer.DefaultSigAlgo(priv), signingConfig) if err != nil { - return Certificate{}, err + return nil, err } cert, err := s.Sign(req) if err != nil { - return Certificate{}, err + return nil, err } - return Certificate{ + return &Certificate{ CA: ca.Cert, Key: key, Cert: cert,