Skip to content

Commit c60a9f2

Browse files
Merge pull request #2003 from vrutkovs/targetCertKeyPair-refreshOnlyWhenExpired
NO-JIRA: needNewTargetCertKeyPair: handle RefreshOnlyWhenExpired
2 parents 7fa221c + 41ad1d5 commit c60a9f2

File tree

2 files changed

+206
-1
lines changed

2 files changed

+206
-1
lines changed

pkg/operator/certrotation/target.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ func needNewTargetCertKeyPair(secret *corev1.Secret, signer *crypto.CA, caBundle
164164
return reason
165165
}
166166

167+
// Exit early if we're only refreshing when expired and the certificate does not need an update
168+
if refreshOnlyWhenExpired {
169+
return ""
170+
}
171+
167172
// check the signer common name against all the common names in our ca bundle so we don't refresh early
168173
signerCommonName := annotations[CertificateIssuer]
169174
if len(signerCommonName) == 0 {

pkg/operator/certrotation/target_test.go

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package certrotation
22

33
import (
44
"context"
5+
"crypto/x509"
56
"crypto/x509/pkix"
6-
clocktesting "k8s.io/utils/clock/testing"
77
"strings"
88
"testing"
99
"time"
1010

11+
clocktesting "k8s.io/utils/clock/testing"
12+
1113
"github.com/davecgh/go-spew/spew"
1214

1315
"github.com/openshift/api/annotations"
@@ -504,3 +506,201 @@ func TestEnsureTargetSignerCertKeyPair(t *testing.T) {
504506
})
505507
}
506508
}
509+
510+
func TestNeedNewTargetCertKeyPair(t *testing.T) {
511+
now := time.Now()
512+
nowFn := func() time.Time { return now }
513+
514+
// Create a test CA certificate
515+
testCA, err := newTestCACertificate(pkix.Name{CommonName: "test-ca"}, int64(1), metav1.Duration{Duration: time.Hour * 24 * 60}, nowFn)
516+
if err != nil {
517+
t.Fatal(err)
518+
}
519+
520+
// Create another CA certificate with different name
521+
otherCA, err := newTestCACertificate(pkix.Name{CommonName: "other-ca"}, int64(2), metav1.Duration{Duration: time.Hour * 24 * 60}, nowFn)
522+
if err != nil {
523+
t.Fatal(err)
524+
}
525+
526+
tests := []struct {
527+
name string
528+
secret *corev1.Secret
529+
signer *crypto.CA
530+
caBundleCerts []*x509.Certificate
531+
refresh time.Duration
532+
refreshOnlyWhenExpired bool
533+
creationRequired bool
534+
expected string
535+
}{
536+
{
537+
name: "secret doesn't exist",
538+
creationRequired: true,
539+
signer: testCA,
540+
caBundleCerts: testCA.Config.Certs,
541+
refresh: time.Hour,
542+
expected: "secret doesn't exist",
543+
},
544+
{
545+
name: "valid secret",
546+
creationRequired: false,
547+
secret: &corev1.Secret{
548+
ObjectMeta: metav1.ObjectMeta{
549+
Annotations: map[string]string{
550+
CertificateNotAfterAnnotation: now.Add(1 * time.Hour).Format(time.RFC3339),
551+
CertificateNotBeforeAnnotation: now.Add(-1 * time.Hour).Format(time.RFC3339),
552+
CertificateIssuer: "test-ca",
553+
},
554+
},
555+
},
556+
signer: testCA,
557+
caBundleCerts: testCA.Config.Certs,
558+
refresh: time.Hour,
559+
expected: "",
560+
},
561+
{
562+
name: "expired secret",
563+
creationRequired: false,
564+
secret: &corev1.Secret{
565+
ObjectMeta: metav1.ObjectMeta{
566+
Annotations: map[string]string{
567+
CertificateNotAfterAnnotation: now.Add(-1 * time.Minute).Format(time.RFC3339),
568+
CertificateNotBeforeAnnotation: now.Add(2 * time.Hour).Format(time.RFC3339),
569+
CertificateIssuer: "test-ca",
570+
},
571+
},
572+
},
573+
signer: testCA,
574+
caBundleCerts: testCA.Config.Certs,
575+
refresh: time.Hour,
576+
expected: "already expired",
577+
},
578+
{
579+
name: "refreshOnlyWhenExpired=true, valid secret",
580+
creationRequired: false,
581+
secret: &corev1.Secret{
582+
ObjectMeta: metav1.ObjectMeta{
583+
Annotations: map[string]string{
584+
CertificateNotAfterAnnotation: now.Add(1 * time.Hour).Format(time.RFC3339), // not expired
585+
CertificateNotBeforeAnnotation: now.Add(-1 * time.Hour).Format(time.RFC3339),
586+
CertificateIssuer: "test-ca",
587+
},
588+
},
589+
},
590+
signer: testCA,
591+
caBundleCerts: testCA.Config.Certs,
592+
refresh: 30 * time.Minute,
593+
refreshOnlyWhenExpired: true,
594+
expected: "", // should not refresh
595+
},
596+
{
597+
name: "refreshOnlyWhenExpired=true, expired secret",
598+
creationRequired: false,
599+
secret: &corev1.Secret{
600+
ObjectMeta: metav1.ObjectMeta{
601+
Annotations: map[string]string{
602+
CertificateNotAfterAnnotation: now.Add(-1 * time.Hour).Format(time.RFC3339), // expired
603+
CertificateNotBeforeAnnotation: now.Add(1 * time.Hour).Format(time.RFC3339),
604+
CertificateIssuer: "test-ca",
605+
},
606+
},
607+
},
608+
signer: testCA,
609+
caBundleCerts: testCA.Config.Certs,
610+
refresh: 30 * time.Minute,
611+
refreshOnlyWhenExpired: true,
612+
expected: "already expired", // needs refresh
613+
},
614+
{
615+
name: "missing issuer name",
616+
creationRequired: false,
617+
secret: &corev1.Secret{
618+
ObjectMeta: metav1.ObjectMeta{
619+
Annotations: map[string]string{
620+
CertificateNotAfterAnnotation: now.Add(2 * time.Hour).Format(time.RFC3339),
621+
CertificateNotBeforeAnnotation: now.Add(-1 * time.Hour).Format(time.RFC3339),
622+
// CertificateIssuer unset
623+
},
624+
},
625+
},
626+
signer: testCA,
627+
caBundleCerts: testCA.Config.Certs,
628+
refresh: time.Hour,
629+
expected: "missing issuer name",
630+
},
631+
{
632+
name: "issuer in ca bundle - valid cert",
633+
creationRequired: false,
634+
secret: &corev1.Secret{
635+
ObjectMeta: metav1.ObjectMeta{
636+
Annotations: map[string]string{
637+
CertificateNotAfterAnnotation: now.Add(2 * time.Hour).Format(time.RFC3339),
638+
CertificateNotBeforeAnnotation: now.Add(-1 * time.Hour).Format(time.RFC3339),
639+
CertificateIssuer: "test-ca",
640+
},
641+
},
642+
},
643+
signer: testCA,
644+
caBundleCerts: testCA.Config.Certs, // test-ca is in the bundle
645+
refresh: time.Hour,
646+
expected: "", // no refresh needed
647+
},
648+
{
649+
name: "issuer not in ca bundle - expired cert",
650+
creationRequired: false,
651+
secret: &corev1.Secret{
652+
ObjectMeta: metav1.ObjectMeta{
653+
Annotations: map[string]string{
654+
CertificateNotAfterAnnotation: now.Add(2 * time.Hour).Format(time.RFC3339),
655+
CertificateNotBeforeAnnotation: now.Add(-1 * time.Hour).Format(time.RFC3339),
656+
CertificateIssuer: "old-ca-name", // not in bundle
657+
},
658+
},
659+
},
660+
signer: testCA,
661+
caBundleCerts: testCA.Config.Certs, // only contains test-ca
662+
refresh: time.Hour,
663+
expected: `issuer "old-ca-name", not in ca bundle:`,
664+
},
665+
{
666+
name: "issuer in ca bundle with multiple certs",
667+
creationRequired: false,
668+
secret: &corev1.Secret{
669+
ObjectMeta: metav1.ObjectMeta{
670+
Annotations: map[string]string{
671+
CertificateNotAfterAnnotation: now.Add(2 * time.Hour).Format(time.RFC3339),
672+
CertificateNotBeforeAnnotation: now.Add(-1 * time.Hour).Format(time.RFC3339),
673+
CertificateIssuer: "other-ca",
674+
},
675+
},
676+
},
677+
signer: testCA,
678+
caBundleCerts: append(testCA.Config.Certs, otherCA.Config.Certs...), // both test-ca and other-ca
679+
refresh: time.Hour,
680+
expected: "", // other-ca is in the bundle
681+
},
682+
}
683+
684+
for _, test := range tests {
685+
t.Run(test.name, func(t *testing.T) {
686+
actual := needNewTargetCertKeyPair(
687+
test.secret,
688+
test.signer,
689+
test.caBundleCerts,
690+
test.refresh,
691+
test.refreshOnlyWhenExpired,
692+
test.creationRequired,
693+
)
694+
695+
if test.expected == "" {
696+
if actual != "" {
697+
t.Errorf("expected no refresh needed, got: %v", actual)
698+
}
699+
} else {
700+
if !strings.Contains(actual, test.expected) {
701+
t.Errorf("expected result to contain %q, got: %v", test.expected, actual)
702+
}
703+
}
704+
})
705+
}
706+
}

0 commit comments

Comments
 (0)