Skip to content
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

[common-hook] Add common CA to tls-certificate #4

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
150 changes: 126 additions & 24 deletions common-hooks/tls-certificate/internal_tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,6 +28,7 @@ const (
keyAlgorithm = "ecdsa"
keySize = 256
SnapshotKey = "secret"
CommonCAKey = "commonSelfsignedCa"
)

type GenSelfSignedTLSHookConf struct {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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)
}
}

Expand All @@ -197,25 +258,60 @@ 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),
Key: string(cert.Key),
}
}

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...),
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion common-hooks/tls-certificate/order_certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/certificate/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
19 changes: 11 additions & 8 deletions pkg/certificate/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"time"

"github.com/cloudflare/cfssl/helpers"
Expand All @@ -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) {
Expand All @@ -51,7 +54,7 @@ func IsCertificateExpiringSoon(cert []byte, durationLeft time.Duration) (bool, e
// modified [email protected]/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")
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions pkg/certificate/hook_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Loading