@@ -35,37 +35,39 @@ import (
3535)
3636
3737const (
38- certName = "tls.crt"
39- keyName = "tls.key"
40- caCertName = "ca.crt"
41- caKeyName = "ca.key"
42- rotationCheckFrequency = 12 * time .Hour
43- certValidityDuration = 10 * 365 * 24 * time .Hour
44- lookaheadInterval = 90 * 24 * time .Hour
38+ certName = "tls.crt"
39+ keyName = "tls.key"
40+ caCertName = "ca.crt"
41+ caKeyName = "ca.key"
42+ rotationCheckFrequency = 12 * time .Hour
43+ defaultCertValidityDuration = 10 * 365 * 24 * time .Hour
44+ lookaheadInterval = 90 * 24 * time .Hour
4545)
4646
4747var crLog = logf .Log .WithName ("cert-rotation" )
4848
49- // WebhookType it the type of webhook, either validating/mutating webhook, a CRD conversion webhook, or an extension API server
49+ // WebhookType it the type of webhook, either validating/mutating webhook, a CRD conversion webhook, or an extension API server.
5050type WebhookType int
5151
5252const (
53- //ValidatingWebhook indicates the webhook is a ValidatingWebhook
53+ // ValidatingWebhook indicates the webhook is a ValidatingWebhook.
5454 Validating WebhookType = iota
55- //MutingWebhook indicates the webhook is a MutatingWebhook
55+ // MutingWebhook indicates the webhook is a MutatingWebhook.
5656 Mutating
57- //CRDConversionWebhook indicates the webhook is a conversion webhook
57+ // CRDConversionWebhook indicates the webhook is a conversion webhook.
5858 CRDConversion
59- //APIServiceWebhook indicates the webhook is an extension API server
59+ // APIServiceWebhook indicates the webhook is an extension API server.
6060 APIService
61- //ExternalDataProvider indicates the webhook is a Gatekeeper External Data Provider
61+ // ExternalDataProvider indicates the webhook is a Gatekeeper External Data Provider.
6262 ExternalDataProvider
6363)
6464
65- var _ manager.Runnable = & CertRotator {}
66- var _ manager.LeaderElectionRunnable = & CertRotator {}
67- var _ manager.Runnable = controllerWrapper {}
68- var _ manager.LeaderElectionRunnable = controllerWrapper {}
65+ var (
66+ _ manager.Runnable = & CertRotator {}
67+ _ manager.LeaderElectionRunnable = & CertRotator {}
68+ _ manager.Runnable = controllerWrapper {}
69+ _ manager.LeaderElectionRunnable = controllerWrapper {}
70+ )
6971
7072type controllerWrapper struct {
7173 controller.Controller
@@ -76,9 +78,9 @@ func (cw controllerWrapper) NeedLeaderElection() bool {
7678 return cw .needLeaderElection
7779}
7880
79- // WebhookInfo is used by the rotator to receive info about resources to be updated with certificates
81+ // WebhookInfo is used by the rotator to receive info about resources to be updated with certificates.
8082type WebhookInfo struct {
81- //Name is the name of the webhook for a validating or mutating webhook, or the CRD name in case of a CRD conversion webhook
83+ // Name is the name of the webhook for a validating or mutating webhook, or the CRD name in case of a CRD conversion webhook
8284 Name string
8385 Type WebhookType
8486}
@@ -114,19 +116,22 @@ func AddRotator(mgr manager.Manager, cr *CertRotator) error {
114116 cr .certsNotMounted = make (chan struct {})
115117 cr .wasCAInjected = atomic .NewBool (false )
116118 cr .caNotInjected = make (chan struct {})
117- if err := mgr .Add (cr ); err != nil {
118- return err
119+ if ! cr .testNoBackgroundRotation {
120+ if err := mgr .Add (cr ); err != nil {
121+ return err
122+ }
119123 }
120124
121125 reconciler := & ReconcileWH {
122- cache : cache ,
123- writer : mgr .GetClient (), // TODO
124- scheme : mgr .GetScheme (),
125- ctx : context .Background (),
126- secretKey : cr .SecretKey ,
127- wasCAInjected : cr .wasCAInjected ,
128- webhooks : cr .Webhooks ,
129- needLeaderElection : cr .RequireLeaderElection ,
126+ cache : cache ,
127+ writer : mgr .GetClient (), // TODO
128+ scheme : mgr .GetScheme (),
129+ ctx : context .Background (),
130+ secretKey : cr .SecretKey ,
131+ wasCAInjected : cr .wasCAInjected ,
132+ webhooks : cr .Webhooks ,
133+ needLeaderElection : cr .RequireLeaderElection ,
134+ refreshCertIfNeededDelegate : cr .refreshCertIfNeeded ,
130135 }
131136 if err := addController (mgr , reconciler ); err != nil {
132137 return err
@@ -186,6 +191,12 @@ type CertRotator struct {
186191 certsNotMounted chan struct {}
187192 wasCAInjected * atomic.Bool
188193 caNotInjected chan struct {}
194+
195+ // testNoBackgroundRotation doesn't actually start the rotator in the background.
196+ // This should only be used for testing.
197+ testNoBackgroundRotation bool
198+ // caCertDuration sets how long a CA cert will be valid for.
199+ caCertDuration time.Duration
189200}
190201
191202func (cr * CertRotator ) NeedLeaderElection () bool {
@@ -204,12 +215,15 @@ func (cr *CertRotator) Start(ctx context.Context) error {
204215 if cr .ExtKeyUsages == nil {
205216 cr .ExtKeyUsages = & []x509.ExtKeyUsage {x509 .ExtKeyUsageServerAuth }
206217 }
218+ if cr .caCertDuration == time .Duration (0 ) {
219+ cr .caCertDuration = defaultCertValidityDuration
220+ }
207221
208222 // explicitly rotate on the first round so that the certificate
209223 // can be bootstrapped, otherwise manager exits before a cert can be written
210224 crLog .Info ("starting cert rotator controller" )
211225 defer crLog .Info ("stopping cert rotator controller" )
212- if err := cr .refreshCertIfNeeded (); err != nil {
226+ if _ , err := cr .refreshCertIfNeeded (); err != nil {
213227 crLog .Error (err , "could not refresh cert on startup" )
214228 return err
215229 }
@@ -224,7 +238,7 @@ tickerLoop:
224238 for {
225239 select {
226240 case <- ticker .C :
227- if err := cr .refreshCertIfNeeded (); err != nil {
241+ if _ , err := cr .refreshCertIfNeeded (); err != nil {
228242 crLog .Error (err , "error rotating certs" )
229243 }
230244 case <- ctx .Done ():
@@ -240,8 +254,11 @@ tickerLoop:
240254 return nil
241255}
242256
243- // refreshCertIfNeeded returns whether there's any error when refreshing the certs if needed.
244- func (cr * CertRotator ) refreshCertIfNeeded () error {
257+ // refreshCertIfNeeded returns true if the CA was rotated
258+ // and if there's any error when rotating the CA or refreshing the certs.
259+ func (cr * CertRotator ) refreshCertIfNeeded () (bool , error ) {
260+ var rotatedCA bool
261+
245262 refreshFn := func () (bool , error ) {
246263 secret := & corev1.Secret {}
247264 if err := cr .reader .Get (context .Background (), cr .SecretKey , secret ); err != nil {
@@ -253,6 +270,7 @@ func (cr *CertRotator) refreshCertIfNeeded() error {
253270 crLog .Error (err , "could not refresh CA and server certs" )
254271 return false , nil
255272 }
273+ rotatedCA = true
256274 crLog .Info ("server certs refreshed" )
257275 if cr .RestartOnSecretRefresh {
258276 crLog .Info ("Secrets have been updated; exiting so pod can be restarted (This behaviour can be changed with the option RestartOnSecretRefresh)" )
@@ -283,16 +301,16 @@ func (cr *CertRotator) refreshCertIfNeeded() error {
283301 Jitter : 1 ,
284302 Steps : 10 ,
285303 }, refreshFn ); err != nil {
286- return err
304+ return rotatedCA , err
287305 }
288- return nil
306+ return rotatedCA , nil
289307}
290308
291309func (cr * CertRotator ) refreshCerts (refreshCA bool , secret * corev1.Secret ) error {
292310 var caArtifacts * KeyPairArtifacts
293311 now := time .Now ()
294312 begin := now .Add (- 1 * time .Hour )
295- end := now .Add (certValidityDuration )
313+ end := now .Add (cr . caCertDuration )
296314 if refreshCA {
297315 var err error
298316 caArtifacts , err = cr .CreateCACert (begin , end )
@@ -458,7 +476,7 @@ func buildArtifactsFromSecret(secret *corev1.Secret) (*KeyPairArtifacts, error)
458476}
459477
460478// CreateCACert creates the self-signed CA cert and private key that will
461- // be used to sign the server certificate
479+ // be used to sign the server certificate.
462480func (cr * CertRotator ) CreateCACert (begin , end time.Time ) (* KeyPairArtifacts , error ) {
463481 templ := & x509.Certificate {
464482 SerialNumber : big .NewInt (0 ),
@@ -496,7 +514,7 @@ func (cr *CertRotator) CreateCACert(begin, end time.Time) (*KeyPairArtifacts, er
496514}
497515
498516// CreateCertPEM takes the results of CreateCACert and uses it to create the
499- // PEM-encoded public certificate and private key, respectively
517+ // PEM-encoded public certificate and private key, respectively.
500518func (cr * CertRotator ) CreateCertPEM (ca * KeyPairArtifacts , begin , end time.Time ) ([]byte , []byte , error ) {
501519 dnsNames := []string {cr .DNSName }
502520 dnsNames = append (dnsNames , cr .ExtraDNSNames ... )
@@ -527,7 +545,7 @@ func (cr *CertRotator) CreateCertPEM(ca *KeyPairArtifacts, begin, end time.Time)
527545 return certPEM , keyPEM , nil
528546}
529547
530- // pemEncode takes a certificate and encodes it as PEM
548+ // pemEncode takes a certificate and encodes it as PEM.
531549func pemEncode (certificateDER []byte , key * rsa.PrivateKey ) ([]byte , []byte , error ) {
532550 certBuf := & bytes.Buffer {}
533551 if err := pem .Encode (certBuf , & pem.Block {Type : "CERTIFICATE" , Bytes : certificateDER }); err != nil {
@@ -620,7 +638,7 @@ func reconcileSecretAndWebhookMapFunc(webhook WebhookInfo, r *ReconcileWH) func(
620638 }
621639}
622640
623- // add adds a new Controller to mgr with r as the reconcile.Reconciler
641+ // add adds a new Controller to mgr with r as the reconcile.Reconciler.
624642func addController (mgr manager.Manager , r * ReconcileWH ) error {
625643 // Create a new controller
626644 c , err := controller .NewUnmanaged ("cert-rotator" , mgr , controller.Options {Reconciler : r })
@@ -657,20 +675,21 @@ func addController(mgr manager.Manager, r *ReconcileWH) error {
657675var _ reconcile.Reconciler = & ReconcileWH {}
658676
659677// ReconcileWH reconciles a validatingwebhookconfiguration, making sure it
660- // has the appropriate CA cert
678+ // has the appropriate CA cert.
661679type ReconcileWH struct {
662- writer client.Writer
663- cache cache.Cache
664- scheme * runtime.Scheme
665- ctx context.Context
666- secretKey types.NamespacedName
667- webhooks []WebhookInfo
668- wasCAInjected * atomic.Bool
669- needLeaderElection bool
680+ writer client.Writer
681+ cache cache.Cache
682+ scheme * runtime.Scheme
683+ ctx context.Context
684+ secretKey types.NamespacedName
685+ webhooks []WebhookInfo
686+ wasCAInjected * atomic.Bool
687+ needLeaderElection bool
688+ refreshCertIfNeededDelegate func () (bool , error )
670689}
671690
672691// Reconcile reads that state of the cluster for a validatingwebhookconfiguration
673- // object and makes sure the most recent CA cert is included
692+ // object and makes sure the most recent CA cert is included.
674693func (r * ReconcileWH ) Reconcile (ctx context.Context , request reconcile.Request ) (reconcile.Result , error ) {
675694 if request .NamespacedName != r .secretKey {
676695 return reconcile.Result {}, nil
@@ -692,6 +711,19 @@ func (r *ReconcileWH) Reconcile(ctx context.Context, request reconcile.Request)
692711 }
693712
694713 if secret .GetDeletionTimestamp ().IsZero () {
714+ if r .refreshCertIfNeededDelegate != nil {
715+ rotatedCA , err := r .refreshCertIfNeededDelegate ()
716+ if err != nil {
717+ crLog .Error (err , "error rotating certs on secret reconcile" )
718+ return reconcile.Result {}, err
719+ }
720+
721+ // if we did rotate the CA, the secret is stale so let's return
722+ if rotatedCA {
723+ return reconcile.Result {}, nil
724+ }
725+ }
726+
695727 artifacts , err := buildArtifactsFromSecret (secret )
696728 if err != nil {
697729 crLog .Error (err , "secret is not well-formed, cannot update webhook configurations" )
0 commit comments