Skip to content
Merged
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
85 changes: 61 additions & 24 deletions common-hooks/tls-certificate/internal_tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import (
)

const (
caExpiryDurationStr = "87600h" // 10 years
caExpiryDuration = (24 * time.Hour) * 365 * 10 // 10 years
caOutdatedDuration = (24 * time.Hour) * 365 / 2 // 6 month, just enough to renew CA certificate
certExpiryDuration = (24 * time.Hour) * 365 * 10 // 10 years
certOutdatedDuration = (24 * time.Hour) * 365 / 2 // 6 month, just enough to renew certificate

Expand Down Expand Up @@ -108,6 +109,21 @@ type GenSelfSignedTLSHookConf struct {
// Canonical name (CN) of common CA certificate.
// If not specified (empty), then (if no CA cert already generated) using CN property of this struct
CommonCACanonicalName string

// CAExpiryDuration is duration of CA certificate validity
// if not specified (zero value), then using default value
CAExpiryDuration time.Duration
// CAOutdatedDuration defines how long before expiration
// a CA certificate is considered outdated and should be renewed.
// If not specified (zero value), then the default value is used.
CAOutdatedDuration time.Duration
// CertExpiryDuration is duration of certificate validity
// if not specified (zero value), then using default value
CertExpiryDuration time.Duration
// CertOutdatedDuration defines how long before expiration
// a certificate is considered outdated and should be renewed.
// If not specified (zero value), then the default value is used.
CertOutdatedDuration time.Duration
}

func (gss GenSelfSignedTLSHookConf) Path() string {
Expand Down Expand Up @@ -171,12 +187,14 @@ func GenSelfSignedTLSConfig(conf GenSelfSignedTLSHookConf) *pkg.HookConfig {
}

type SelfSignedCertValues struct {
CA *certificate.Authority
CN string
KeyAlgorithm string
KeySize int
SANs []string
Usages []string
CA *certificate.Authority
CN string
KeyAlgorithm string
KeySize int
SANs []string
Usages []string
CAExpireDuration time.Duration
CertExpireDuration time.Duration
}

func GenSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, input *pkg.HookInput) error {
Expand Down Expand Up @@ -217,6 +235,23 @@ func GenSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i
panic(fmt.Errorf("bad KeySize/KeyAlgorithm combination: %w", err))
}

caExpiryDuration := caExpiryDuration
if conf.CAExpiryDuration != 0 {
caExpiryDuration = conf.CAExpiryDuration
}
caOutdatedDuration := caOutdatedDuration
if conf.CAOutdatedDuration != 0 {
caOutdatedDuration = conf.CAOutdatedDuration
}
certExpiryDuration := certExpiryDuration
if conf.CertExpiryDuration != 0 {
certExpiryDuration = conf.CertExpiryDuration
}
certOutdatedDuration := certOutdatedDuration
if conf.CertOutdatedDuration != 0 {
certOutdatedDuration = conf.CertOutdatedDuration
}

return func(_ context.Context, input *pkg.HookInput) error {
if conf.BeforeHookCheck != nil {
passed := conf.BeforeHookCheck(input)
Expand Down Expand Up @@ -246,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())
auth, err = getCommonCA(input, conf.CommonCAPath(), caOutdatedDuration)
if err != nil {
commonCACanonicalName := conf.CommonCACanonicalName

Expand All @@ -258,7 +293,7 @@ func GenSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i
commonCACanonicalName,
certificate.WithKeyAlgo(keyAlgorithm),
certificate.WithKeySize(keySize),
certificate.WithCAExpiry(caExpiryDurationStr))
certificate.WithCAExpiry(caExpiryDuration))
if err != nil {
return fmt.Errorf("generate ca: %w", err)
}
Expand All @@ -285,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)
caOutdated, err := isOutdatedCA(cert.CA, caOutdatedDuration)
if err != nil {
input.Logger.Warn("is outdated ca", log.Err(err))
}
Expand All @@ -297,7 +332,7 @@ func GenSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i
caOutdated = true
}

certOutdated, err := isIrrelevantCert(cert.Cert, sans)
certOutdated, err := isIrrelevantCert(cert.Cert, sans, certOutdatedDuration)
if err != nil {
input.Logger.Warn("is irrelevant cert", log.Err(err))
}
Expand All @@ -309,12 +344,14 @@ func GenSelfSignedTLS(conf GenSelfSignedTLSHookConf) func(ctx context.Context, i

if mustGenerate {
cert, err = generateNewSelfSignedTLS(SelfSignedCertValues{
CA: auth,
CN: cn,
KeyAlgorithm: keyAlgorithm,
KeySize: keySize,
SANs: sans,
Usages: usages,
CA: auth,
CN: cn,
KeyAlgorithm: keyAlgorithm,
KeySize: keySize,
SANs: sans,
Usages: usages,
CAExpireDuration: caExpiryDuration,
CertExpireDuration: certExpiryDuration,
})

if err != nil {
Expand Down Expand Up @@ -347,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) (*certificate.Authority, error) {
func getCommonCA(input *pkg.HookInput, valKey string, caOutdatedDuration time.Duration) (*certificate.Authority, error) {
auth := new(certificate.Authority)

ca, ok := input.Values.GetOk(valKey)
Expand All @@ -360,7 +397,7 @@ func getCommonCA(input *pkg.HookInput, valKey string) (*certificate.Authority, e
return nil, err
}

outdated, err := isOutdatedCA(auth.Cert)
outdated, err := isOutdatedCA(auth.Cert, caOutdatedDuration)
if err != nil {
input.Logger.Error("is outdated ca", log.Err(err))

Expand All @@ -386,7 +423,7 @@ func generateNewSelfSignedTLS(input SelfSignedCertValues) (*certificate.Certific
input.CN,
certificate.WithKeyAlgo(input.KeyAlgorithm),
certificate.WithKeySize(input.KeySize),
certificate.WithCAExpiry(caExpiryDurationStr))
certificate.WithCAExpiry(input.CAExpireDuration))
if err != nil {
return nil, fmt.Errorf("generate ca: %w", err)
}
Expand All @@ -398,7 +435,7 @@ func generateNewSelfSignedTLS(input SelfSignedCertValues) (*certificate.Certific
certificate.WithSANs(input.SANs...),
certificate.WithKeyAlgo(input.KeyAlgorithm),
certificate.WithKeySize(input.KeySize),
certificate.WithSigningDefaultExpiry(certExpiryDuration),
certificate.WithSigningDefaultExpiry(input.CertExpireDuration),
certificate.WithSigningDefaultUsage(input.Usages),
)
if err != nil {
Expand All @@ -409,7 +446,7 @@ func generateNewSelfSignedTLS(input SelfSignedCertValues) (*certificate.Certific
}

// check certificate duration and SANs list
func isIrrelevantCert(certData []byte, desiredSANSs []string) (bool, error) {
func isIrrelevantCert(certData []byte, desiredSANSs []string, certOutdatedDuration time.Duration) (bool, error) {
cert, err := certificate.ParseCertificate(certData)
if err != nil {
return false, fmt.Errorf("parse certificate: %w", err)
Expand Down Expand Up @@ -446,7 +483,7 @@ func isIrrelevantCert(certData []byte, desiredSANSs []string) (bool, error) {
return false, nil
}

func isOutdatedCA(ca []byte) (bool, error) {
func isOutdatedCA(ca []byte, 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 {
Expand All @@ -458,7 +495,7 @@ func isOutdatedCA(ca []byte) (bool, error) {
return false, fmt.Errorf("parse certificate: %w", err)
}

if time.Until(cert.NotAfter) < certOutdatedDuration {
if time.Until(cert.NotAfter) < caOutdatedDuration {
return true, nil
}

Expand Down
22 changes: 19 additions & 3 deletions common-hooks/tls-certificate/internal_tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
mock "github.com/deckhouse/module-sdk/testing/mock"
)

const tenYears = (24 * time.Hour) * 365 * 10

func Test_JQFilterTLS(t *testing.T) {
t.Run("apply tls", func(t *testing.T) {
const rawSecret = `
Expand Down Expand Up @@ -99,16 +101,26 @@ func Test_GenSelfSignedTLS(t *testing.T) {
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.IsCA, true)
assert.Equal(t, ca.NotAfter.Sub(ca.NotBefore), time.Hour*24*365*5)

cert, err := certificate.ParseCertificate([]byte(values.Crt))
assert.NoError(t, err)

assert.Equal(t, ca.Subject, cert.Issuer)

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), time.Hour*24*365)
})

var input = &pkg.HookInput{
Expand All @@ -133,6 +145,8 @@ func Test_GenSelfSignedTLS(t *testing.T) {
"%PUBLIC_DOMAIN%://example-webhook.d8-example-module.svc",
}),
FullValuesPathPrefix: "d8-example-module.internal.webhookCert",
CAExpiryDuration: time.Hour * 24 * 365 * 5,
CertExpiryDuration: time.Hour * 24 * 365,
}

err := tlscertificate.GenSelfSignedTLS(config)(context.Background(), input)
Expand All @@ -150,7 +164,7 @@ func Test_GenSelfSignedTLS(t *testing.T) {
"cert-name",
certificate.WithKeyAlgo("ecdsa"),
certificate.WithKeySize(256),
certificate.WithCAExpiry("87600h"))
certificate.WithCAExpiry(tenYears))

assert.NoError(t, err)

Expand Down Expand Up @@ -247,7 +261,7 @@ func Test_GenSelfSignedTLS(t *testing.T) {
"cert-name",
certificate.WithKeyAlgo("ecdsa"),
certificate.WithKeySize(256),
certificate.WithCAExpiry("87600h"))
certificate.WithCAExpiry(tenYears))

assert.NoError(t, err)

Expand Down Expand Up @@ -327,6 +341,7 @@ func Test_GenSelfSignedTLS(t *testing.T) {
"%PUBLIC_DOMAIN%://example-webhook.d8-example-module.svc",
}),
FullValuesPathPrefix: "d8-example-module.internal.webhookCert",
CertOutdatedDuration: 2 * time.Hour,
}

err := tlscertificate.GenSelfSignedTLS(config)(context.Background(), input)
Expand All @@ -344,7 +359,7 @@ func Test_GenSelfSignedTLS(t *testing.T) {
"cert-name",
certificate.WithKeyAlgo("ecdsa"),
certificate.WithKeySize(256),
certificate.WithCAExpiry("1h"))
certificate.WithCAExpiry(time.Hour))

assert.NoError(t, err)

Expand Down Expand Up @@ -424,6 +439,7 @@ func Test_GenSelfSignedTLS(t *testing.T) {
"%PUBLIC_DOMAIN%://example-webhook.d8-example-module.svc",
}),
FullValuesPathPrefix: "d8-example-module.internal.webhookCert",
CAOutdatedDuration: 2 * time.Hour,
}

err := tlscertificate.GenSelfSignedTLS(config)(context.Background(), input)
Expand Down
10 changes: 7 additions & 3 deletions pkg/certificate/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ limitations under the License.

package certificate

import "github.com/cloudflare/cfssl/csr"
import (
"time"

"github.com/cloudflare/cfssl/csr"
)

type Option func(request *csr.CertificateRequest)

Expand All @@ -32,9 +36,9 @@ func WithKeySize(size int) Option {
}
}

func WithCAExpiry(expiry string) Option {
func WithCAExpiry(expiry time.Duration) Option {
return func(request *csr.CertificateRequest) {
request.CA.Expiry = expiry
request.CA.Expiry = expiry.String()
}
}

Expand Down