diff --git a/charts/cert-management/templates/cert.gardener.cloud_certificates.yaml b/charts/cert-management/templates/cert.gardener.cloud_certificates.yaml index f6984c4f..76266f0e 100644 --- a/charts/cert-management/templates/cert.gardener.cloud_certificates.yaml +++ b/charts/cert-management/templates/cert.gardener.cloud_certificates.yaml @@ -88,6 +88,15 @@ spec: items: type: string type: array + duration: + description: |- + Requested 'duration' (i.e. lifetime) of the Certificate. Note that the + ACME issuer may choose to ignore the requested duration, just like any other + requested attribute. + If unset, this defaults to 90 days (2160h). + Must be greater than twice of the renewal window + Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration. + type: string ensureRenewedAfter: description: EnsureRenewedAfter specifies a time stamp in the past. Renewing is only triggered if certificate notBefore date is before diff --git a/pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml b/pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml index 20238376..b327fec0 100644 --- a/pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml +++ b/pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml @@ -83,6 +83,15 @@ spec: items: type: string type: array + duration: + description: |- + Requested 'duration' (i.e. lifetime) of the Certificate. Note that the + ACME issuer may choose to ignore the requested duration, just like any other + requested attribute. + If unset, this defaults to 90 days (2160h). + Must be greater than twice of the renewal window + Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration. + type: string ensureRenewedAfter: description: EnsureRenewedAfter specifies a time stamp in the past. Renewing is only triggered if certificate notBefore date is before diff --git a/pkg/apis/cert/crds/zz_generated_crds.go b/pkg/apis/cert/crds/zz_generated_crds.go index 96f78282..abc43836 100644 --- a/pkg/apis/cert/crds/zz_generated_crds.go +++ b/pkg/apis/cert/crds/zz_generated_crds.go @@ -387,6 +387,15 @@ spec: items: type: string type: array + duration: + description: |- + Requested 'duration' (i.e. lifetime) of the Certificate. Note that the + ACME issuer may choose to ignore the requested duration, just like any other + requested attribute. + If unset, this defaults to 90 days (2160h). + Must be greater than twice of the renewal window + Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration. + type: string ensureRenewedAfter: description: EnsureRenewedAfter specifies a time stamp in the past. Renewing is only triggered if certificate notBefore date is before diff --git a/pkg/apis/cert/v1alpha1/types.go b/pkg/apis/cert/v1alpha1/types.go index 2e68a3f1..9a17918d 100644 --- a/pkg/apis/cert/v1alpha1/types.go +++ b/pkg/apis/cert/v1alpha1/types.go @@ -85,6 +85,14 @@ type CertificateSpec struct { // Private key options. These include the key algorithm and size. // +optional PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` + // Requested 'duration' (i.e. lifetime) of the Certificate. Note that the + // ACME issuer may choose to ignore the requested duration, just like any other + // requested attribute. + // If unset, this defaults to 90 days (2160h). + // Must be greater than twice of the renewal window + // Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration. + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` } // IssuerRef is the reference of the issuer by name. diff --git a/pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go index 347a04f7..121734aa 100644 --- a/pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go @@ -453,6 +453,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) { *out = new(CertificatePrivateKey) (*in).DeepCopyInto(*out) } + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(metav1.Duration) + **out = **in + } return } diff --git a/pkg/cert/legobridge/certificate.go b/pkg/cert/legobridge/certificate.go index 0c8466c7..942b739b 100644 --- a/pkg/cert/legobridge/certificate.go +++ b/pkg/cert/legobridge/certificate.go @@ -63,6 +63,8 @@ type ObtainInput struct { PreferredChain string // KeyType represents the algo and size to use for the private key (only used if CSR is not set). KeyType certcrypto.KeyType + // Duration is the lifetime of the certificate + Duration *time.Duration } // DNSControllerSettings are the settings for the DNSController. @@ -520,12 +522,12 @@ func newCASignedCertFromInput(input ObtainInput) (*certificate.Resource, error) if err != nil { return nil, err } - return newCASignedCertFromCertReq(csr, input.CAKeyPair) + return newCASignedCertFromCertReq(csr, input.CAKeyPair, input.Duration) } // newCASignedCertFromCertReq returns a new Certificate signed by a CA based on // an x509.CertificateRequest and a CA key pair. A private key will be generated. -func newCASignedCertFromCertReq(csr *x509.CertificateRequest, CAKeyPair *TLSKeyPair) (*certificate.Resource, error) { +func newCASignedCertFromCertReq(csr *x509.CertificateRequest, CAKeyPair *TLSKeyPair, duration *time.Duration) (*certificate.Resource, error) { pubKeySize := pubKeySize(csr.PublicKey) if pubKeySize == 0 { pubKeySize = defaultKeySize(csr.PublicKeyAlgorithm) @@ -534,7 +536,10 @@ func newCASignedCertFromCertReq(csr *x509.CertificateRequest, CAKeyPair *TLSKeyP if err != nil { return nil, err } - return issueSignedCert(csr, false, privKey, privKeyPEM, CAKeyPair) + if duration == nil { + return nil, fmt.Errorf("duration must be set") + } + return issueSignedCert(csr, false, privKey, privKeyPEM, CAKeyPair, *duration) } // RevokeCertificate revokes a certificate diff --git a/pkg/cert/legobridge/pki.go b/pkg/cert/legobridge/pki.go index a6a26bf8..998f43b6 100644 --- a/pkg/cert/legobridge/pki.go +++ b/pkg/cert/legobridge/pki.go @@ -62,12 +62,12 @@ const ( ) // issueSignedCert does all the Certificate Issuing. -func issueSignedCert(csr *x509.CertificateRequest, isCA bool, privKey crypto.Signer, privKeyPEM []byte, signerKeyPair *TLSKeyPair) (*certificate.Resource, error) { +func issueSignedCert(csr *x509.CertificateRequest, isCA bool, privKey crypto.Signer, privKeyPEM []byte, signerKeyPair *TLSKeyPair, duration time.Duration) (*certificate.Resource, error) { csrPEM, err := generateCSRPEM(csr, privKey) if err != nil { return nil, err } - crt, err := generateCertFromCSR(csrPEM, DefaultCertDuration, isCA) + crt, err := generateCertFromCSR(csrPEM, duration, isCA) if err != nil { return nil, err } diff --git a/pkg/controller/issuer/certificate/reconciler.go b/pkg/controller/issuer/certificate/reconciler.go index 87c218bd..0b0dfb33 100644 --- a/pkg/controller/issuer/certificate/reconciler.go +++ b/pkg/controller/issuer/certificate/reconciler.go @@ -30,6 +30,7 @@ import ( apierrrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/utils/ptr" api "github.com/gardener/cert-management/pkg/apis/cert/v1alpha1" "github.com/gardener/cert-management/pkg/cert/legobridge" @@ -402,7 +403,9 @@ func (r *certReconciler) obtainCertificateAndPendingACME(logctx logger.LogContex if err != nil { return r.failed(logctx, obj, api.StateError, err) } - + if cert.Spec.Duration != nil { + return r.failedStop(logctx, obj, api.StateError, fmt.Errorf("duration cannot be set for ACME certificate")) + } err = r.validateDomainsAndCsr(&cert.Spec, issuer.Spec.ACME.Domains, issuerKey) if err != nil { return r.failedStop(logctx, obj, api.StateError, err) @@ -555,6 +558,17 @@ func (r *certReconciler) obtainCertificateCA(logctx logger.LogContext, obj resou return r.failed(logctx, obj, api.StateError, err) } + duration, err := r.getDuration(cert) + if err != nil { + return r.failedStop(logctx, obj, api.StateError, err) + } + if duration == nil { + duration = ptr.To(2 * legobridge.DefaultCertDuration) + } + err = r.validateCertDuration(duration, CAKeyPair) + if err != nil { + return r.failedStop(logctx, obj, api.StateError, err) + } err = r.validateDomainsAndCsr(&cert.Spec, nil, issuerKey) if err != nil { return r.failedStop(logctx, obj, api.StateError, err) @@ -584,7 +598,7 @@ func (r *certReconciler) obtainCertificateCA(logctx logger.LogContext, obj resou input := legobridge.ObtainInput{CAKeyPair: CAKeyPair, IssuerKey: issuerKey, CommonName: cert.Spec.CommonName, DNSNames: cert.Spec.DNSNames, CSR: cert.Spec.CSR, - Callback: callback, Renew: renew} + Callback: callback, Renew: renew, Duration: duration} err = r.obtainer.Obtain(input) if err != nil { @@ -643,6 +657,29 @@ func (r *certReconciler) checkDomainRangeRestriction(issuerDomains *api.DNSSelec return nil } +func (r *certReconciler) getDuration(cert *api.Certificate) (*time.Duration, error) { + if cert.Spec.Duration == nil { + return nil, nil + } + duration := cert.Spec.Duration.Duration + if duration < 2*r.renewalWindow { + return nil, fmt.Errorf("certificate duration must be greater than %v", 2*r.renewalWindow) + } + return ptr.To(duration), nil +} + +func (r *certReconciler) validateCertDuration(duration *time.Duration, caKeyPair *legobridge.TLSKeyPair) error { + if duration == nil { + return nil + } + caNotAfter := caKeyPair.Cert.NotAfter + now := time.Now() + if now.Add(*duration).After(caNotAfter) { + return fmt.Errorf("certificate lifetime (%v) is longer than the lifetime of the CA certificate (%v)", now.Add(*duration), caNotAfter) + } + return nil +} + func (r *certReconciler) loadSecret(secretRef *corev1.SecretReference) (*corev1.Secret, error) { secretObjectName := resources.NewObjectName(secretRef.Namespace, secretRef.Name) secret := &corev1.Secret{}