@@ -3,6 +3,7 @@ package certificate
3
3
import (
4
4
"context"
5
5
"errors"
6
+ "math/rand"
6
7
"sync"
7
8
"time"
8
9
@@ -16,9 +17,10 @@ import (
16
17
)
17
18
18
19
// NewManager creates a new CertificateManager with the passed MRCClient and options
19
- func NewManager (ctx context.Context , mrcClient MRCClient , serviceCertValidityDuration time.Duration , msgBroker * messaging.Broker , checkInterval time.Duration ) (* Manager , error ) {
20
+ func NewManager (ctx context.Context , mrcClient MRCClient , getServiceCertValidityPeriod func () time. Duration , getIngressCertValidityDuration func () time.Duration , msgBroker * messaging.Broker , checkInterval time.Duration ) (* Manager , error ) {
20
21
m := & Manager {
21
- serviceCertValidityDuration : serviceCertValidityDuration ,
22
+ serviceCertValidityDuration : getServiceCertValidityPeriod ,
23
+ ingressCertValidityDuration : getIngressCertValidityDuration ,
22
24
msgBroker : msgBroker ,
23
25
}
24
26
@@ -152,75 +154,119 @@ func (m *Manager) GetTrustDomain() string {
152
154
return m .signingIssuer .TrustDomain
153
155
}
154
156
157
+ // shouldRotate determines whether a certificate should be rotated.
158
+ func (m * Manager ) shouldRotate (c * Certificate ) bool {
159
+ // The certificate is going to expire at a timestamp T
160
+ // We want to renew earlier. How much earlier is defined in renewBeforeCertExpires.
161
+ // We add a few seconds noise to the early renew period so that certificates that may have been
162
+ // created at the same time are not renewed at the exact same time.
163
+ intNoise := rand .Intn (noiseSeconds ) // #nosec G404
164
+ secondsNoise := time .Duration (intNoise ) * time .Second
165
+ renewBefore := RenewBeforeCertExpires + secondsNoise
166
+ if time .Until (c .GetExpiration ()) <= renewBefore {
167
+ log .Info ().Msgf ("Cert %s should be rotated; expires in %+v; renewBefore is %+v" ,
168
+ c .GetCommonName (),
169
+ time .Until (c .GetExpiration ()),
170
+ renewBefore )
171
+ return true
172
+ }
173
+
174
+ m .mu .Lock ()
175
+ validatingIssuer := m .validatingIssuer
176
+ signingIssuer := m .signingIssuer
177
+ m .mu .Unlock ()
178
+
179
+ // During root certificate rotation the Issuers will change. If the Manager's Issuers are
180
+ // different than the validating Issuer and signing Issuer IDs in the certificate, the
181
+ // certificate must be reissued with the correct Issuers for the current rotation stage and
182
+ // state. If there is no root certificate rotation in progress, the cert and Manager Issuers
183
+ // will match.
184
+ if c .signingIssuerID != signingIssuer .ID || c .validatingIssuerID != validatingIssuer .ID {
185
+ log .Info ().Msgf ("Cert %s should be rotated; in progress root certificate rotation" ,
186
+ c .GetCommonName ())
187
+ return true
188
+ }
189
+ return false
190
+ }
191
+
155
192
func (m * Manager ) checkAndRotate () {
156
193
// NOTE: checkAndRotate can reintroduce a certificate that has been released, thereby creating an unbounded cache.
157
194
// A certificate can also have been rotated already, leaving the list of issued certs stale, and we re-rotate.
158
195
// the latter is not a bug, but a source of inefficiency.
159
-
160
196
certs := map [string ]* Certificate {}
161
197
m .cache .Range (func (keyIface interface {}, certInterface interface {}) bool {
162
198
key := keyIface .(string )
163
199
certs [key ] = certInterface .(* Certificate )
164
200
return true // continue the iteration
165
201
})
166
- for key , cert := range certs {
167
- shouldRotate := cert .ShouldRotate ()
168
-
169
- word := map [bool ]string {true : "will" , false : "will not" }[shouldRotate ]
170
- log .Trace ().Msgf ("Cert %s %s be rotated; expires in %+v; renewBeforeCertExpires is %+v" ,
171
- cert .GetCommonName (),
172
- word ,
173
- time .Until (cert .GetExpiration ()),
174
- RenewBeforeCertExpires )
175
-
176
- if shouldRotate {
177
- opts := []IssueOption {WithValidityPeriod (m .serviceCertValidityDuration )}
178
- // if the key is equal to the common name, then it was issued with FullCNProvided(). This will prevent
179
- // an additional trust domain from being appended. We don't do this in every case, in case the trust domain
180
- // has changed since the last issue.
181
- if key == cert .CommonName .String () {
182
- opts = append (opts , FullCNProvided ())
183
- }
184
- newCert , err := m .IssueCertificate (key , opts ... )
185
- if err != nil {
186
- // TODO(#3962): metric might not be scraped before process restart resulting from this error
187
- log .Error ().Err (err ).Str (errcode .Kind , errcode .GetErrCodeWithMetric (errcode .ErrRotatingCert )).
188
- Msgf ("Error rotating cert SerialNumber=%s" , cert .GetSerialNumber ())
189
- continue
190
- }
191
202
192
- m . msgBroker . GetCertPubSub (). Pub (events. PubSubMessage {
193
- Kind : announcements . CertificateRotated ,
194
- NewObj : newCert ,
195
- OldObj : cert ,
196
- }, announcements . CertificateRotated . String ())
203
+ for key , cert := range certs {
204
+ opts := [] IssueOption {}
205
+ if key == cert . CommonName . String () {
206
+ opts = append ( opts , FullCNProvided ())
207
+ }
197
208
198
- log .Debug ().Msgf ("Rotated certificate (old SerialNumber=%s) with new SerialNumber=%s" , cert .SerialNumber , newCert .SerialNumber )
209
+ _ , err := m .IssueCertificate (key , cert .certType , opts ... )
210
+ if err != nil {
211
+ log .Error ().Err (err ).Str (errcode .Kind , errcode .GetErrCodeWithMetric (errcode .ErrRotatingCert )).
212
+ Msgf ("Error rotating cert SerialNumber=%s" , cert .GetSerialNumber ())
199
213
}
200
214
}
201
215
}
202
216
217
+ func (m * Manager ) getValidityDurationForCertType (ct CertType ) time.Duration {
218
+ switch ct {
219
+ case Internal :
220
+ return constants .OSMCertificateValidityPeriod
221
+ case IngressGateway :
222
+ return m .ingressCertValidityDuration ()
223
+ case Service :
224
+ return m .serviceCertValidityDuration ()
225
+ default :
226
+ log .Debug ().Msgf ("Unknown certificate type %s provided when getting validity duration" , ct )
227
+ return constants .OSMCertificateValidityPeriod
228
+ }
229
+ }
230
+
231
+ // getFromCache returns the certificate with the specified cn from cache if it exists.
232
+ // Note: getFromCache might return an expired or invalid certificate.
203
233
func (m * Manager ) getFromCache (key string ) * Certificate {
204
234
certInterface , exists := m .cache .Load (key )
205
235
if ! exists {
206
236
return nil
207
237
}
208
238
cert := certInterface .(* Certificate )
209
239
log .Trace ().Msgf ("Certificate found in cache SerialNumber=%s" , cert .GetSerialNumber ())
210
- if cert .ShouldRotate () {
211
- log .Trace ().Msgf ("Certificate found in cache but has expired SerialNumber=%s" , cert .GetSerialNumber ())
212
- return nil
213
- }
214
240
return cert
215
241
}
216
242
217
- // IssueCertificate implements Manager and returns a newly issued certificate from the given client.
218
- func (m * Manager ) IssueCertificate (prefix string , opts ... IssueOption ) (* Certificate , error ) {
219
- var err error
220
- cert := m .getFromCache (prefix ) // Don't call this while holding the lock
243
+ // IssueCertificate returns a newly issued certificate from the given client
244
+ // or an existing valid certificate from the local cache.
245
+ func (m * Manager ) IssueCertificate (prefix string , ct CertType , opts ... IssueOption ) (* Certificate , error ) {
246
+ // a singleflight group is used here to ensure that only one issueCertificate is in
247
+ // flight at a time for a given certificate prefix. Helps avoid a race condition if
248
+ // issueCertificate is called multiple times in a row for the same certificate prefix.
249
+ cert , err , _ := m .group .Do (prefix , func () (interface {}, error ) {
250
+ return m .issueCertificate (prefix , ct , opts ... )
251
+ })
252
+ if err != nil {
253
+ return nil , err
254
+ }
255
+ return cert .(* Certificate ), nil
256
+ }
221
257
222
- options := defaultOptions (m .serviceCertValidityDuration )
258
+ func (m * Manager ) issueCertificate (prefix string , ct CertType , opts ... IssueOption ) (* Certificate , error ) {
259
+ var rotate bool
260
+ cert := m .getFromCache (prefix ) // Don't call this while holding the lock
261
+ if cert != nil {
262
+ // check if cert needs to be rotated
263
+ rotate = m .shouldRotate (cert )
264
+ if ! rotate {
265
+ return cert , nil
266
+ }
267
+ }
223
268
269
+ options := & issueOptions {}
224
270
for _ , o := range opts {
225
271
o (options )
226
272
}
@@ -231,27 +277,40 @@ func (m *Manager) IssueCertificate(prefix string, opts ...IssueOption) (*Certifi
231
277
m .mu .Unlock ()
232
278
233
279
start := time .Now ()
234
- if cert == nil || cert .signingIssuerID != signingIssuer .ID || cert .validatingIssuerID != validatingIssuer .ID {
235
- cert , err = signingIssuer .IssueCertificate (options .formatCN (prefix , signingIssuer .TrustDomain ), options .validityPeriod )
236
- if err != nil {
237
- return nil , err
238
- }
239
280
240
- // if we have different signing and validating issuers,
241
- // create the cert's trust context
242
- if validatingIssuer . ID != signingIssuer . ID {
243
- cert = cert . newMergedWithRoot ( validatingIssuer . CertificateAuthority )
244
- }
281
+ validityDuration := m . getValidityDurationForCertType ( ct )
282
+ newCert , err := signingIssuer . IssueCertificate ( options . formatCN ( prefix , signingIssuer . TrustDomain ), validityDuration )
283
+ if err != nil {
284
+ return nil , err
285
+ }
245
286
246
- cert .signingIssuerID = signingIssuer .ID
247
- cert .validatingIssuerID = validatingIssuer .ID
287
+ // if we have different signing and validating issuers,
288
+ // create the cert's trust context
289
+ if validatingIssuer .ID != signingIssuer .ID {
290
+ newCert = newCert .newMergedWithRoot (validatingIssuer .CertificateAuthority )
248
291
}
249
292
250
- m .cache .Store (prefix , cert )
293
+ newCert .signingIssuerID = signingIssuer .ID
294
+ newCert .validatingIssuerID = validatingIssuer .ID
251
295
252
- log .Trace ().Msgf ("It took %s to issue certificate with SerialNumber=%s" , time .Since (start ), cert .GetSerialNumber ())
296
+ newCert .certType = ct
297
+
298
+ m .cache .Store (prefix , newCert )
299
+
300
+ log .Trace ().Msgf ("It took %s to issue certificate with SerialNumber=%s" , time .Since (start ), newCert .GetSerialNumber ())
301
+
302
+ if rotate {
303
+ // Certificate was rotated
304
+ m .msgBroker .GetCertPubSub ().Pub (events.PubSubMessage {
305
+ Kind : announcements .CertificateRotated ,
306
+ NewObj : newCert ,
307
+ OldObj : cert ,
308
+ }, announcements .CertificateRotated .String ())
309
+
310
+ log .Debug ().Msgf ("Rotated certificate (old SerialNumber=%s) with new SerialNumber=%s" , cert .SerialNumber , newCert .SerialNumber )
311
+ }
253
312
254
- return cert , nil
313
+ return newCert , nil
255
314
}
256
315
257
316
// ReleaseCertificate is called when a cert will no longer be needed and should be removed from the system.
0 commit comments