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

feat: Introduce certificate duration #354

Merged
merged 8 commits into from
Nov 12, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions pkg/apis/cert/crds/zz_generated_crds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/cert/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions pkg/cert/legobridge/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/cert/legobridge/pki.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
41 changes: 39 additions & 2 deletions pkg/controller/issuer/certificate/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
marc1404 marked this conversation as resolved.
Show resolved Hide resolved
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{}
Expand Down